diff --git a/scripts/markdown.js b/scripts/markdown.js index 033a27c..e33d6c6 100644 --- a/scripts/markdown.js +++ b/scripts/markdown.js @@ -1,237 +1,11 @@ #!/usr/bin/env node -class MarkdownRenderer { - constructor() { - this.renderers = {}; - MarkdownRenderer.instance = this; - } - static get current() { - return this.instance; - } - registerRenderer(name, renderer) { - this.renderers[name] = renderer; - } - render(node) { - switch (node.type) { - case 'text': - return this.text(node); - case 'role': - return this.role(node); - case 'title': - return this.title(node); - case 'block_role': - return this.block_role(node); - case 'root': - return this.root(node); - case 'paragraph': - return this.paragraph(node); - default: - throw new Error(`Unknown node type: ${node.type}`); - } - } - paragraph(node) { - return `${node.children.map((it) => this.render(it)).join('')}\n\n`; - } - text(node) { - return node.self; - } - role(node) { - const renderer = this.renderers[node.name]; - if (renderer) { - return renderer.render(node); - } - else { - return node.children.map((it) => this.render(it)).join(''); - } - } - title(node) { - return `${'#'.repeat(node.level)} ${node.children.map((it) => this.render(it)).join('')}\n`; - } - block_role(node) { - const renderer = this.renderers[node.name]; - if (renderer) { - const result = renderer.render(node); - return result ? `\n${result}\n` : ''; - } - else { - return `
${node.children.map((it) => this.render(it)).join('')}
`; - } - } - root(node) { - return `${node.children.map((it) => this.render(it)).join('')}`; - } -} - -function escape(str) { - return str.replace(/[`]/g, function (s) { - return ({ - '`': '\\`' - }[s] || s); - }); -} - -const anchor = (renderer) => ({ - render(node) { - return `[${node.children.map((it) => renderer.render(it)).join('')}](${node.args[0] || node.args['url']})`; - } -}); - -const code = (renderer) => ({ - render(node) { - if (node.type === 'role') { - return `\`${escape(node.children.map((it) => renderer.render(it)).join(''))}\``; - } - return `\`\`\`\n${node.children.map((it) => renderer.render(it)).join('')}\`\`\``; - } -}); - -const image = (renderer) => ({ - render(node) { - const href = node.args[0] || node.args['url']; - const alt = node.args[1] || node.args['alt']; - const title = node.args[2] || node.args['title']; - return `![${alt || ''}${title ? ` "${title}"` : ''}](${href})`; - } -}); - -const decorations = (renderer) => ({ - render(node) { - switch (node.name) { - case 'underlined': - case 'u': - return `_${node.children - .map((it) => renderer.render(it)) - .join('')}_`; - case 'strike': - case 's': - return `~~${node.children - .map((it) => renderer.render(it)) - .join('')}~~`; - case 'italic': - case 'i': - return `*${node.children - .map((it) => renderer.render(it)) - .join('')}*`; - case 'bold': - case 'b': - return `**${node.children - .map((it) => renderer.render(it)) - .join('')}**`; - case 'br': - return ` \n`; - default: - return `${renderer.render(node)}`; - } - } -}); - -const id = (renderer) => ({ - render(node) { - const id = node.args[0] || node.args['id']; - if (node.type === 'role') { - return `${node.children - .map((it) => renderer.render(it)) - .join('')}`; - } - return `
${node.children - .map((it) => renderer.render(it)) - .join('')}
`; - } -}); - -const list = (renderer) => ({ - render(node, type) { - type = type || node.args[0] || node.args['type'] || 'ordered'; - - if (node.type === 'role') { - return `${node.children - .map((it) => renderer.render(it)) - .join('')}`; - } - if (node.name === 'list') { - return node.children - .map((it) => this.render(it)) - .join('') - .trim(); - } - const bullet = type === 'ordered' ? `1.` : '-'; - - return `${bullet} ${node.children - .map((it) => renderer.render(it)) - .join('') - .replace(/\n/g, '\n ')}\n`; - } -}); - -const table = (renderer) => ({ - render(node) { - switch (node.name) { - case 'table': - return this.renderTable(node); - case 'th': - return this.renderHeading(node); - case 'tr': - return this.renderRow(node); - case 'td': - return this.renderColumn(node); - default: - return `
${renderer.render(node)}
`; - } - }, - renderTable(node) { - return node.children - .map((it) => renderer.render(it)) - .join('') - .replace(/\n\n/g, '\n'); - }, - renderHeading(node) { - return `|${node.children - .map((it) => renderer.render(it, false)) - .join('|') - .replace(/\n/g, '
')}|\n|${':-:|'.repeat(node.children.length)}`; - }, - renderRow(node) { - return `|${node.children - .map((it) => this.renderColumn(it, true)) - .join('|') - .replace(/\n/g, '
')}|`; - }, - renderColumn(node) { - return node.children - .map((it) => renderer.render(it)) - .join(''); - } -}); - (function () { const fs = require('fs'); const limp = require('../lib'); const args = [...process.argv]; - const renderer = new MarkdownRenderer(); - - renderer.registerRenderer('ref', anchor(renderer)); - renderer.registerRenderer('code', code(renderer)); - renderer.registerRenderer('image', image(renderer)); - renderer.registerRenderer('img', image(renderer)); - renderer.registerRenderer('bold', decorations(renderer)); - renderer.registerRenderer('italic', decorations(renderer)); - renderer.registerRenderer('underlined', decorations(renderer)); - renderer.registerRenderer('strike', decorations(renderer)); - renderer.registerRenderer('b', decorations(renderer)); - renderer.registerRenderer('i', decorations(renderer)); - renderer.registerRenderer('u', decorations(renderer)); - renderer.registerRenderer('s', decorations(renderer)); - renderer.registerRenderer('id', id(renderer)); - renderer.registerRenderer('label', id(renderer)); - renderer.registerRenderer('list', list(renderer)); - renderer.registerRenderer('item', list(renderer)); - renderer.registerRenderer('table', table(renderer)); - renderer.registerRenderer('th', table(renderer)); - renderer.registerRenderer('tr', table(renderer)); - renderer.registerRenderer('td', table(renderer)); - for (; args.length > 0;) { if (args.shift() === __filename) { break; @@ -249,10 +23,7 @@ const table = (renderer) => ({ } const file = arg.match(/^(.*?)(?:\.[^.]+)?$/)[1]; - - const ast = limp.parseDocument(fs.readFileSync(arg, 'utf8')); - - const out = renderer.render(ast) + const out = limp.compileToMarkdown(fs.readFileSync(arg, 'utf8')) fs.writeFileSync(`${file}.md`, out); } diff --git a/src/index.ts b/src/index.ts index 82072d7..627af37 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import htmlRenderer from './render/html'; +import markdownRenderer from './render/markdown'; import { parseDocument } from './parsing/parsing'; export * from './parsing/parsing'; @@ -7,3 +8,7 @@ export * from './parsing/strparsing'; export function compileToHTML(src: string): string { return htmlRenderer.render(parseDocument(src)); } + +export function compileToMarkdown(src: string): string { + return markdownRenderer.render(parseDocument(src)); +} diff --git a/src/render/markdown/index.ts b/src/render/markdown/index.ts new file mode 100644 index 0000000..9f7ed77 --- /dev/null +++ b/src/render/markdown/index.ts @@ -0,0 +1,11 @@ +import renderer from './markdownRenderer'; +import './renderers/textDecorations'; +import './renderers/id'; +import './renderers/anchor'; +import './renderers/image'; +import './renderers/list'; +import './renderers/code'; +import './renderers/table'; +import './renderers/comment'; + +export default renderer; diff --git a/src/render/markdown/markdownRenderer.ts b/src/render/markdown/markdownRenderer.ts new file mode 100644 index 0000000..dae7364 --- /dev/null +++ b/src/render/markdown/markdownRenderer.ts @@ -0,0 +1,47 @@ +import LimpRenderer from '..'; +import { LimpNodeOf } from '../..'; + +export class MarkdownRenderer extends LimpRenderer { + paragraph(node: LimpNodeOf<'paragraph'>): string { + return `${node.children.map((it) => this.render(it)).join('')}\n\n`; + } + + text(node: LimpNodeOf<'text'>): string { + return node.self; + } + + role(node: LimpNodeOf<'role'>): string { + const renderer = this.renderers[node.name]; + if (renderer) { + return renderer.render(node); + } else { + return node.children.map((it) => this.render(it)).join(''); + } + } + + title(node: LimpNodeOf<'title'>): string { + return `${'#'.repeat(node.level)} ${node.children + .map((it) => this.render(it)) + .join('')}\n`; + } + + block_role(node: LimpNodeOf<'block_role'>): string { + const renderer = this.renderers[node.name]; + if (renderer) { + const result = renderer.render(node); + return result ? `\n${result}\n` : ''; + } else { + return `
${node.children + .map((it) => this.render(it)) + .join('')}
`; + } + } + + root(node: LimpNodeOf<'root'>): string { + return `${node.children.map((it) => this.render(it)).join('')}`; + } +} + +const renderer = new MarkdownRenderer(); + +export default renderer; diff --git a/src/render/markdown/renderers/anchor.ts b/src/render/markdown/renderers/anchor.ts new file mode 100644 index 0000000..1c12e13 --- /dev/null +++ b/src/render/markdown/renderers/anchor.ts @@ -0,0 +1,13 @@ +import { Renderer } from '../..'; +import { LimpNodeOf } from '../../../parsing/parsing'; +import renderer from '../markdownRenderer'; + +class AnchorRoleRenderer implements Renderer { + render(node: LimpNodeOf<'role' | 'block_role'>): string { + return `[${node.children.map((it) => renderer.render(it)).join('')}](${node.args[0] || node.args['url']})`; + } +} + +renderer.registerRenderer('ref', new AnchorRoleRenderer()); + +export default {}; diff --git a/src/render/markdown/renderers/code.ts b/src/render/markdown/renderers/code.ts new file mode 100644 index 0000000..0038810 --- /dev/null +++ b/src/render/markdown/renderers/code.ts @@ -0,0 +1,21 @@ +import { Renderer } from '../..'; +import { LimpNodeOf } from '../../../parsing/parsing'; +import { escape } from '../utils'; +import renderer from '../markdownRenderer'; + +class CodeRoleRenderer implements Renderer { + render(node: LimpNodeOf<'role' | 'block_role'>): string { + if (node.type === 'role') { + return `\`${escape( + node.children.map((it) => renderer.render(it)).join('') + )}\``; + } + return `\`\`\`\n${node.children + .map((it) => renderer.render(it)) + .join('')}\`\`\``; + } +} + +renderer.registerRenderer('code', new CodeRoleRenderer()); + +export default {}; diff --git a/src/render/markdown/renderers/comment.ts b/src/render/markdown/renderers/comment.ts new file mode 100644 index 0000000..3a52d5a --- /dev/null +++ b/src/render/markdown/renderers/comment.ts @@ -0,0 +1,32 @@ +import { Renderer } from '../..'; +import { LimpNodeOf } from '../../../parsing/parsing'; +import { escape } from '../utils'; +import renderer from '../markdownRenderer'; + +class CommentRoleRenderer implements Renderer { + render(node: LimpNodeOf<'role' | 'block_role'>): string { + if (this.isNoEmit(node)) { + return ''; + } + return ``; + } + + isNoEmit(node: LimpNodeOf<'role' | 'block_role'>): boolean { + const trues = ['yes', 'y', 'true', 'noemit']; + + return ( + node.args[0] === 'noemit' || + 'noemit' in node.args || + trues.includes(node.args.noemit) + ); + } +} + +const instance = new CommentRoleRenderer(); + +renderer.registerRenderer('rem', instance); +renderer.registerRenderer('comment', instance); + +export default {}; diff --git a/src/render/markdown/renderers/id.ts b/src/render/markdown/renderers/id.ts new file mode 100644 index 0000000..d24df94 --- /dev/null +++ b/src/render/markdown/renderers/id.ts @@ -0,0 +1,25 @@ +import { Renderer } from '../..'; +import { LimpNodeOf } from '../../../parsing/parsing'; +import renderer from '../markdownRenderer'; + +class LabelRoleRenderer implements Renderer { + render(node: LimpNodeOf<'role' | 'block_role'>): string { + const id = node.args[0] || node.args['id']; + + if (node.type === 'role') { + return `${node.children + .map((it) => renderer.render(it)) + .join('')}`; + } + return `
${node.children + .map((it) => renderer.render(it)) + .join('')}
`; + } +} + +const instance = new LabelRoleRenderer(); + +renderer.registerRenderer('id', instance); +renderer.registerRenderer('label', instance); + +export default {}; diff --git a/src/render/markdown/renderers/image.ts b/src/render/markdown/renderers/image.ts new file mode 100644 index 0000000..0b26adb --- /dev/null +++ b/src/render/markdown/renderers/image.ts @@ -0,0 +1,19 @@ +import { Renderer } from '../..'; +import { LimpNodeOf } from '../../../parsing/parsing'; +import renderer from '../markdownRenderer'; + +class ImageRoleRenderer implements Renderer { + render(node: LimpNodeOf<'role' | 'block_role'>): string { + const href = node.args[0] || node.args['url']; + const alt = node.args[1] || node.args['alt']; + const title = node.args[2] || node.args['title']; + return `![${alt || ''}${title ? ` "${title}"` : ''}](${href})`; + } +} + +const instance = new ImageRoleRenderer(); + +renderer.registerRenderer('image', instance); +renderer.registerRenderer('img', instance); + +export default {}; diff --git a/src/render/markdown/renderers/list.ts b/src/render/markdown/renderers/list.ts new file mode 100644 index 0000000..7c6d188 --- /dev/null +++ b/src/render/markdown/renderers/list.ts @@ -0,0 +1,40 @@ +import { Renderer } from '../..'; +import { LimpNodeOf, LimpNode } from '../../../parsing/parsing'; +import renderer from '../markdownRenderer'; + +class ListRenderer implements Renderer { + render(node: LimpNodeOf<'role' | 'block_role'>): string { + return this.renderInternal(node); + } + + renderInternal(node: LimpNode, type?: string): string { + if (node.type !== 'block_role') { + return `${node.children + .map((it) => renderer.render(it)) + .join('')}`; + } + + type = type || node.args[0] || node.args['type'] || 'ordered'; + + if (node.name === 'list') { + return node.children + .map((it) => this.renderInternal(it as any, type)) + .join('') + .trim(); + } + + const bullet = type === 'ordered' ? `1.` : '-'; + + return `${bullet} ${node.children + .map((it) => renderer.render(it)) + .join('') + .replace(/\n/g, '\n ')}\n`; + } +} + +const instance = new ListRenderer(); + +renderer.registerRenderer('list', instance); +renderer.registerRenderer('item', instance); + +export default {}; diff --git a/src/render/markdown/renderers/table.ts b/src/render/markdown/renderers/table.ts new file mode 100644 index 0000000..4939118 --- /dev/null +++ b/src/render/markdown/renderers/table.ts @@ -0,0 +1,60 @@ +import { Renderer } from '../..'; +import { LimpNodeOf, LimpNode } from '../../../parsing/parsing'; +import renderer from '../markdownRenderer'; + +class TableRenderer implements Renderer { + render(node: LimpNodeOf<'role' | 'block_role'>): string { + if (node.type === 'role') { + return `${renderer.render( + node + )}`; + } + + switch (node.name) { + case 'table': + return this.renderTable(node); + case 'th': + return this.renderHeading(node); + case 'tr': + return this.renderRow(node); + default: + return `
${renderer.render( + node + )}
`; + } + } + + renderTable(node: LimpNodeOf<'block_role'>): string { + return node.children + .map((it) => renderer.render(it)) + .join('') + .replace(/\n\n/g, '\n'); + } + + renderHeading(node: LimpNodeOf<'block_role'>): string { + return `|${node.children + .map((it) => this.renderColumn(it)) + .join('|') + .replace(/\n/g, '
')}|\n|${':-:|'.repeat(node.children.length)}`; + } + + renderRow(node: LimpNodeOf<'block_role'>): string { + return `|${node.children + .map((it) => this.renderColumn(it)) + .join('|') + .replace(/\n/g, '
')}|`; + } + + renderColumn(node: LimpNode): string { + return node.children.map((it) => renderer.render(it)).join(''); + } +} + +const instance = new TableRenderer(); + +renderer.registerRenderer('table', instance); +renderer.registerRenderer('th', instance); +renderer.registerRenderer('tr', instance); +renderer.registerRenderer('td', instance); + +export default {}; diff --git a/src/render/markdown/renderers/textDecorations.ts b/src/render/markdown/renderers/textDecorations.ts new file mode 100644 index 0000000..dd41068 --- /dev/null +++ b/src/render/markdown/renderers/textDecorations.ts @@ -0,0 +1,42 @@ +import { Renderer } from '../..'; +import { LimpNodeOf } from '../../../parsing/parsing'; +import renderer from '../markdownRenderer'; + +class TextDecorationsRenderer implements Renderer { + render(node: LimpNodeOf<'role' | 'block_role'>): string { + switch (node.name) { + case 'underlined': + case 'u': + return `_${node.children.map((it) => renderer.render(it)).join('')}_`; + case 'strike': + case 's': + return `~~${node.children.map((it) => renderer.render(it)).join('')}~~`; + case 'italic': + case 'i': + return `*${node.children.map((it) => renderer.render(it)).join('')}*`; + case 'bold': + case 'b': + return `**${node.children.map((it) => renderer.render(it)).join('')}**`; + case 'br': + return ` \n`; + default: + return `${renderer.render( + node + )}`; + } + } +} + +const instance = new TextDecorationsRenderer(); + +renderer.registerRenderer('bold', instance); +renderer.registerRenderer('underlined', instance); +renderer.registerRenderer('italic', instance); +renderer.registerRenderer('strike', instance); +renderer.registerRenderer('br', instance); +renderer.registerRenderer('b', instance); +renderer.registerRenderer('u', instance); +renderer.registerRenderer('i', instance); +renderer.registerRenderer('s', instance); + +export default {}; diff --git a/src/render/markdown/utils.ts b/src/render/markdown/utils.ts new file mode 100644 index 0000000..a85f7bb --- /dev/null +++ b/src/render/markdown/utils.ts @@ -0,0 +1,9 @@ +export function escape(str: string): string { + return str.replace(/[`]/g, function (s) { + return ( + { + '`': '\\`', + }[s] || s + ); + }); +}