Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"commander": "^12.1.0",
"eo2js": "^0.0.8",
"fast-xml-parser": "^5.2.3",
"marked": "^4.3.0",
"node": "^24.1.0",
"relative": "^3.0.2",
"semver": "^7.7.2",
Expand Down
51 changes: 32 additions & 19 deletions src/commands/docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
const fs = require('fs');
const path = require('path');
const SaxonJS = require('saxon-js');
const { marked } = require('marked');

/**
* Recursively reads all .xmir files from a directory.
Expand Down Expand Up @@ -52,6 +53,17 @@ function transformDocument(xmir, xsl) {
return html;
}

/**
* Converts Markdown blocks in documentation to HTML
* @param {String} html - text of HTML file
* @return {String} HTML document
*/
function convertMarkdownToHtml(html) {
const regex = /(?<opening_tag><div\s+class\s*=\s*["']object-desc["'][^>]*>)(?<content>[\s\S]*?)(?<closing_tag><\/div>)/gi;
const converted_html = html.replace(regex, (match, opening_tag, content, closing_tag) => `${opening_tag}${marked.parse(content)}${closing_tag}`);
return converted_html;
}

/**
* Creates documentation block from given XMIR
* @param {String} xmir_path - path of XMIR
Expand All @@ -61,7 +73,7 @@ function createXmirHtmlBlock(xmir_path) {
try {
const xmir = fs.readFileSync(xmir_path).toString();
const xsl = fs.readFileSync(path.join(__dirname, '..', 'resources', 'xmir-transformer.xsl')).toString();
return transformDocument(xmir, xsl);
return convertMarkdownToHtml(transformDocument(xmir, xsl));
} catch(error) {
throw new Error(`Error while applying XSL to XMIR: ${error.message}`, error);
}
Expand All @@ -75,37 +87,38 @@ function createXmirHtmlBlock(xmir_path) {
* @return {String} HTML of the package
*/
function generatePackageHtml(name, xmir_htmls, css_path) {
const title = `<h1 class="package-title">Package ${name} documentation</h1>`;
xmir_htmls = xmir_htmls.filter(item => item !== '<article class="app-block"></article>');
const cur_date = new Date();
return `<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tacit-css.min.css"/>
<link href="${css_path}" rel="stylesheet" type="text/css">
${title}
</head>
<body>
${xmir_htmls.join('\n')}
<section>
<header>
<nav>
<h1>${name} documentation</h1>
<p>Creation date: ${cur_date.toUTCString()}</p>
</nav>
</header>
${xmir_htmls.join('\n')}
</section>
</body>
</html>`;
}

/**
* Wraps given html body
* @param {String} name - File name
* @param {String} html - HTML body
* @param {String} css_path - CSS file path
* @return {String} Ready HTML
*/
function wrapHtml(html, css_path) {
return `
<!DOCTYPE html>
<html>
<head>
<link href="${css_path}" rel="stylesheet" type="text/css">
</head>
<body>
${html}
</body>
</html>
`;
function wrapHtml(name, html, css_path) {
return generatePackageHtml(name, [html], css_path);
}

/**
Expand All @@ -128,7 +141,7 @@ module.exports = async function(opts) {
const xmir_html = createXmirHtmlBlock(xmir);
const html_app = path.join(output, path.dirname(relative),`${name}.html`);
fs.mkdirSync(path.dirname(html_app), {recursive: true});
fs.writeFileSync(html_app, wrapHtml(xmir_html, css));
fs.writeFileSync(html_app, wrapHtml(name, xmir_html, css));
const packages = path.dirname(relative).split(path.sep).join('.');
const html_package = path.join(output, `package_${packages}.html`);
if (!(packages in packages_info)) {
Expand All @@ -143,10 +156,10 @@ module.exports = async function(opts) {
for (const package_name of Object.keys(packages_info)) {
fs.mkdirSync(path.dirname(packages_info[package_name].path), {recursive: true});
fs.writeFileSync(packages_info[package_name].path,
generatePackageHtml(package_name, packages_info[package_name].xmir_htmls, css));
generatePackageHtml(`${package_name} package`, packages_info[package_name].xmir_htmls, css));
}
const packages = path.join(output, 'packages.html');
fs.writeFileSync(packages, generatePackageHtml('', all_xmir_htmls, css));
fs.writeFileSync(packages, generatePackageHtml('overall package', all_xmir_htmls, css));
console.info('Documentation generation completed in the %s directory', output);
} catch (error) {
console.error('Error generating documentation:', error);
Expand Down
13 changes: 7 additions & 6 deletions src/resources/xmir-transformer.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
* SPDX-License-Identifier: MIT
-->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html"/>
<xsl:template match="/">
<div class="app-block">
<xsl:for-each select="//o[(@name and @name != 'φ') and (not(@base) or (@base != '∅' and @base != 'ξ'))]">
<article class="app-block">
<xsl:for-each select="//o[(@name and (@name != 'φ' and @name != 'λ' and not(starts-with(@name, '+')))) and (not(@base) or (@base != '∅' and @base != 'ξ'))]">
<xsl:if test="//comments/comment[@line = current()/@line]">
<div class="object-block">
<xsl:variable name="fullname">
Expand All @@ -17,23 +18,23 @@
</xsl:choose>
</xsl:for-each>
</xsl:variable>
<h2 class="object-title"><xsl:value-of select="$fullname"/></h2>
<h1 class="object-title"><xsl:value-of select="$fullname"/></h1>
<p class="object-sign">
<xsl:value-of select="$fullname"/>(<xsl:for-each select="current()/o[@base and @base = '∅']">
<xsl:value-of select="@name"/>
<xsl:choose>
<xsl:when test="position() != last()">, </xsl:when>
</xsl:choose>
</xsl:for-each>)</p>
<p class="object-desc">
<div class="object-desc">
<xsl:call-template name="break">
<xsl:with-param name="text" select="//comments/comment[@line = current()/@line]"/>
</xsl:call-template>
</p>
</div>
</div>
</xsl:if>
</xsl:for-each>
</div>
</article>
</xsl:template>
<xsl:template name="break">
<xsl:param name="text" select="string(.)"/>
Expand Down
69 changes: 69 additions & 0 deletions test/commands/test_docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,73 @@ describe('docs', () => {
assert(!test_content.includes('Not docs'), `Unnecessary comment found in ${test_html}`);
done();
});
/**
* Tests that the 'docs' command does not generate test to HTML.
* @param {Mocha.Done} done - Mocha callback signaling asynchronous completion
*/
it('does not generate tests to HTML', (done) => {
const sample = parsed;
fs.mkdirSync(sample, {recursive: true});
const xmir = path.join(sample, 'test.xmir');
fs.writeFileSync(xmir, fs.readFileSync(path.join(__dirname, '..', 'resources', 'test4.xmir')).toString());
runSync([
'docs',
'--verbose',
'-s', path.resolve(home, 'src'),
'-t', home,
]);
assert(fs.existsSync(docs), 'Expected the docs directory to be created but it is missing');
const test_html = path.join(docs, 'test.html');
assert(fs.existsSync(test_html), `Expected file ${test_html} but it was not created`);
const test_content = fs.readFileSync(test_html);
assert(!test_content.includes('Tests this comment is not in docs.'), `Unnecessary comment found in ${test_html}`);
done();
});
/**
* Tests that the 'docs' command does not generate completely empty HTML for empty docblocks.
* @param {Mocha.Done} done - Mocha callback signaling asynchronous completion
*/
it('does not generate empty HTML for empty docblocks', (done) => {
const sample = parsed;
fs.mkdirSync(sample, {recursive: true});
const xmir = path.join(sample, 'test.xmir');
fs.writeFileSync(xmir, fs.readFileSync(path.join(__dirname, '..', 'resources', 'test5.xmir')).toString());
runSync([
'docs',
'--verbose',
'-s', path.resolve(home, 'src'),
'-t', home,
]);
assert(fs.existsSync(docs), 'Expected the docs directory to be created but it is missing');
const test_html = path.join(docs, 'test.html');
assert(fs.existsSync(test_html), `Expected file ${test_html} but it was not created`);
const test_content = fs.readFileSync(test_html).toString();
const text_only = test_content.replace(/<[^>]*>/g, '')
.replace(/\s+/g, '');
assert(text_only.length > 0);
done();
});
/**
* Tests that the 'docs' command generates markdown correctly.
* @param {Mocha.Done} done - Mocha callback signaling asynchronous completion
*/
it('generates markdown correctly', (done) => {
const sample = parsed;
fs.mkdirSync(sample, {recursive: true});
const xmir = path.join(sample, 'test.xmir');
fs.writeFileSync(xmir, fs.readFileSync(path.join(__dirname, '..', 'resources', 'test6.xmir')).toString());
runSync([
'docs',
'--verbose',
'-s', path.resolve(home, 'src'),
'-t', home,
]);
assert(fs.existsSync(docs), 'Expected the docs directory to be created but it is missing');
const test_html = path.join(docs, 'test.html');
assert(fs.existsSync(test_html), `Expected file ${test_html} but it was not created`);
const test_content = fs.readFileSync(test_html);
assert(test_content.includes('<strong>Strong test</strong>'), `Markdown not processed correctly in ${test_html}`);
assert(test_content.includes('<code>Code test</code>'), `Markdown not processed correctly in ${test_html}`);
done();
});
});
56 changes: 56 additions & 0 deletions test/resources/test4.xmir
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
* SPDX-FileCopyrightText: Copyright (c) 2022-2025 Objectionary.com
* SPDX-License-Identifier: MIT
-->
<object xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
author="eo-parser"
dob="2025-08-21T14:45:38"
ms="988"
revision="16c0818"
time="2025-10-08T12:27:13.669248Z"
version="0.58.2"
xsi:noNamespaceSchemaLocation="https://www.eolang.org/xsd/XMIR-0.58.2.xsd">
<listing># Says hello to Jeff.
[] &amp;gt; app
# Tests this comment is not in docs.
[] +&amp;gt; some-check
QQ.io.stdout &amp;gt; @
&amp;quot;Test passed!&amp;quot;
</listing>
<comments>
<comment line="2">Says hello to Jeff.</comment>
<comment line="4">Tests this comment is not in docs.</comment>
</comments>
<o line="2" name="app" pos="0">
<o line="4" name="+some-check" pos="2">
<o base="Φ.org.eolang.io.stdout" line="5" name="φ" pos="9">
<o as="α0" base="Φ.org.eolang.string" line="6" pos="6">
<o as="α0" base="Φ.org.eolang.bytes" line="6" pos="6">
<o as="α0" line="6" pos="6">54-65-73-74-20-70-61-73-73-65-64-21</o>
</o>
</o>
</o>
</o>
</o>
<sheets>
<sheet>validate-before-stars</sheet>
<sheet>resolve-before-stars</sheet>
<sheet>wrap-method-calls</sheet>
<sheet>const-to-dataized</sheet>
<sheet>stars-to-tuples</sheet>
<sheet>vars-float-up</sheet>
<sheet>move-voids-up</sheet>
<sheet>validate-objects-count</sheet>
<sheet>build-fqns</sheet>
<sheet>expand-qqs</sheet>
<sheet>expand-aliases</sheet>
<sheet>resolve-aliases</sheet>
<sheet>add-default-package</sheet>
<sheet>roll-bases</sheet>
<sheet>cti-adds-errors</sheet>
<sheet>decorate</sheet>
<sheet>add-as-attributes-inside-application</sheet>
<sheet>st-hex</sheet>
</sheets>
</object>
49 changes: 49 additions & 0 deletions test/resources/test5.xmir
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
* SPDX-FileCopyrightText: Copyright (c) 2022-2025 Objectionary.com
* SPDX-License-Identifier: MIT
-->
<object xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
author="eo-parser"
dob="2025-09-15T13:34:49"
ms="1126"
revision="aa0072f"
time="2025-10-08T12:54:07.764342Z"
version="0.58.6"
xsi:noNamespaceSchemaLocation="https://www.eolang.org/xsd/XMIR-0.58.6.xsd">
<listing>[args] &amp;gt; app
QQ.io.stdout &amp;gt; @
&amp;quot;Hello, world!\n&amp;quot;
</listing>
<o line="1" name="app" pos="0">
<o base="ξ" line="1" name="xi🌵" pos="0"/>
<o base="∅" line="1" name="args" pos="1"/>
<o base="Φ.org.eolang.io.stdout" line="2" name="φ" pos="7">
<o as="α0" base="Φ.org.eolang.string" line="3" pos="4">
<o as="α0" base="Φ.org.eolang.bytes" line="3" pos="4">
<o as="α0" line="3" pos="4">48-65-6C-6C-6F-2C-20-77-6F-72-6C-64-21-0A</o>
</o>
</o>
</o>
</o>
<sheets>
<sheet>validate-before-stars</sheet>
<sheet>resolve-before-stars</sheet>
<sheet>wrap-method-calls</sheet>
<sheet>const-to-dataized</sheet>
<sheet>stars-to-tuples</sheet>
<sheet>vars-float-up</sheet>
<sheet>move-voids-up</sheet>
<sheet>validate-objects-count</sheet>
<sheet>build-fqns</sheet>
<sheet>expand-qqs</sheet>
<sheet>expand-aliases</sheet>
<sheet>resolve-aliases</sheet>
<sheet>add-default-package</sheet>
<sheet>roll-bases</sheet>
<sheet>cti-adds-errors</sheet>
<sheet>decorate</sheet>
<sheet>add-as-attributes-inside-application</sheet>
<sheet>st-hex</sheet>
</sheets>
</object>
54 changes: 54 additions & 0 deletions test/resources/test6.xmir
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
* SPDX-FileCopyrightText: Copyright (c) 2022-2025 Objectionary.com
* SPDX-License-Identifier: MIT
-->
<object xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
author="eo-parser"
dob="2025-09-15T13:34:49"
ms="1005"
revision="aa0072f"
time="2025-10-09T02:03:24.421186Z"
version="0.58.6"
xsi:noNamespaceSchemaLocation="https://www.eolang.org/xsd/XMIR-0.58.6.xsd">
<listing># **Strong test**
# `Code test`
[args] &amp;gt; app
QQ.io.stdout &amp;gt; @
&amp;quot;Test passed!&amp;quot;
</listing>
<comments>
<comment line="3">**Strong test**\n`Code test`</comment>
</comments>
<o line="3" name="app" pos="0">
<o base="ξ" line="3" name="xi🌵" pos="0"/>
<o base="∅" line="3" name="args" pos="1"/>
<o base="Φ.org.eolang.io.stdout" line="4" name="φ" pos="7">
<o as="α0" base="Φ.org.eolang.string" line="5" pos="4">
<o as="α0" base="Φ.org.eolang.bytes" line="5" pos="4">
<o as="α0" line="5" pos="4">54-65-73-74-20-70-61-73-73-65-64-21</o>
</o>
</o>
</o>
</o>
<sheets>
<sheet>validate-before-stars</sheet>
<sheet>resolve-before-stars</sheet>
<sheet>wrap-method-calls</sheet>
<sheet>const-to-dataized</sheet>
<sheet>stars-to-tuples</sheet>
<sheet>vars-float-up</sheet>
<sheet>move-voids-up</sheet>
<sheet>validate-objects-count</sheet>
<sheet>build-fqns</sheet>
<sheet>expand-qqs</sheet>
<sheet>expand-aliases</sheet>
<sheet>resolve-aliases</sheet>
<sheet>add-default-package</sheet>
<sheet>roll-bases</sheet>
<sheet>cti-adds-errors</sheet>
<sheet>decorate</sheet>
<sheet>add-as-attributes-inside-application</sheet>
<sheet>st-hex</sheet>
</sheets>
</object>