diff --git a/packages/posttext/src/std/resolvers.ts b/packages/posttext/src/std/resolvers.ts new file mode 100644 index 0000000..702f917 --- /dev/null +++ b/packages/posttext/src/std/resolvers.ts @@ -0,0 +1,563 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import Prism from 'prismjs' +import loadLanguages from 'prismjs/components/index.js' +import stripIndent from 'strip-indent' +import qrcode from 'qrcode' +import katex from 'katex' + +import { Resolver } from '../resolver' + +const KATEX_STATE = Symbol('KatexState') +const CODE_BLOCK_STATE = Symbol('CodeBlockState') + +export const TOC = Symbol('Toc') +export const tocDef = [ + null, + 'section', + 'subsection', + 'subsubsection', +] + +export const resolvers = ( + _options: RegistryOptions +): Record => { + return { + __root__: { + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + const renderer = context.plugins.get('renderer') + + const content = renderer.render(tag) + + await writer.write(content) + }, + }, + + posttext: { + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + const title = tag.attrs.title + + await writer.metadata.add({ + title, + }) + }, + }, + + comment: { + tag: async (): Promise => { + /* pass */ + }, + }, + + title: { + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + const content = tag.blocks.index(0).text() + + await writer.write(`

${content}

`) + }, + }, + + section: { + data: { + [TOC]: async (context: any, tag: any): Promise => { + const content = tag.blocks.index(0).text() + + return { + type: 'section', + content, + } + }, + }, + + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + const content = tag.blocks.index(0).text() + + await writer.write(`

${content}

`) + }, + }, + + subsection: { + data: { + [TOC]: async (context: any, tag: any): Promise => { + const content = tag.blocks.index(0).text() + + return { + type: 'subsection', + content, + } + }, + }, + + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + const content = tag.blocks.index(0).text() + + await writer.write(`

${content}

`) + }, + }, + + subsubsection: { + data: { + [TOC]: async (context: any, tag: any): Promise => { + const content = tag.blocks.index(0).text() + + return { + type: 'subsubsection', + content, + } + }, + }, + + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + const content = tag.blocks.index(0).text() + + await writer.write(`

${content}

`) + }, + }, + + bold: { + data: { + [TOC]: async (context: any, tag: any): Promise => { + const content = tag.blocks.index(0).text() + + return { + type: 'bold', + content, + } + }, + }, + + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + const content = tag.blocks.index(0).text() + + await writer.write( + `${content?.replace(/\s\s\n/g, '
') ?? ''}
` + ) + }, + }, + + italic: { + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + const content = tag.blocks.index(0).text() + + await writer.write( + `${content?.replace(/\s\s\n/g, '
') ?? ''}
` + ) + }, + }, + + underline: { + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + const content = tag.blocks.index(0).text() + + await writer.write( + `${content?.replace(/\s\s\n/g, '
') ?? ''}
` + ) + }, + }, + + list: { + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + const content = tag.blocks.index(0).text() + + await writer.write(`
    ${content}
`) + }, + }, + + item: { + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + const content: string | undefined = tag.blocks + .index(0) + .text() + + await writer.write( + `
  • ${ + content?.replace(/\s\s\n/g, '
    ') ?? '' + }
  • ` + ) + }, + }, + + code: { + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + + const languageLoaded = + context.state[CODE_BLOCK_STATE]?.languagesLoaded ?? + false + if (!languageLoaded) { + context.state[CODE_BLOCK_STATE] = { + languagesLoaded: true, + } + + loadLanguages() + + writer.addDependency({ + type: 'css', + src: 'prismjs/themes/prism.css', + }) + } + + const language = + tag.params.length > 0 && + Object.keys(Prism.languages).indexOf( + tag.params.index(0) + ) + ? tag.params.index(0) + : 'text' + + const BEGIN_NEWLINE = /^\r?\n/ + const END_NEWLINE = /\r?\n[\t ]+$/ + + const content = tag.blocks.index(0).text() + const strippedContent: string = content + ? stripIndent(content) + .replace(BEGIN_NEWLINE, '') + .replace(END_NEWLINE, '') + : '' + + const code: string = + language !== 'text' + ? Prism.highlight( + strippedContent, + Prism.languages[language], + language + ) + : strippedContent + + await writer.write( + `
    ${code}
    ` + ) + }, + }, + + "code'": { + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + + const languageLoaded = + context.state[CODE_BLOCK_STATE]?.languagesLoaded ?? + false + if (!languageLoaded) { + context.state[CODE_BLOCK_STATE] = { + languagesLoaded: true, + } + + loadLanguages() + + writer.addDependency({ + type: 'css', + src: 'prismjs/themes/prism.css', + }) + } + + const language = + tag.params.length > 0 && + Object.keys(Prism.languages).indexOf( + tag.params.index(0) + ) + ? tag.params.index(0) + : 'text' + + const BEGIN_NEWLINE = /^\r?\n/ + const END_NEWLINE = /\r?\n[\t ]+$/ + + const content = tag.blocks.index(0).text() + const strippedContent: string = content + ? stripIndent(content) + .replace(BEGIN_NEWLINE, '') + .replace(END_NEWLINE, '') + : '' + + const code: string = + language !== 'text' + ? Prism.highlight( + strippedContent, + Prism.languages[language], + language + ) + : strippedContent + + await writer.write( + `${code}` + ) + }, + }, + + katex: { + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + const katexLoaded = + context.state[KATEX_STATE]?.katexLoaded ?? false + if (!katexLoaded) { + context.state[KATEX_STATE] = { + katexLoaded: true, + } + + await writer.addDependency({ + type: 'css', + src: 'katex/dist/katex.css', + }) + } + + const rawContent = tag.blocks.index(0).text() + + // TODO: Handle errors + const content = katex.renderToString(rawContent, { + throwOnError: false, + displayMode: true, + }) + + await writer.write(content) + }, + }, + + "katex'": { + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + const katexLoaded = + context.state[KATEX_STATE]?.katexLoaded ?? false + if (!katexLoaded) { + context.state[KATEX_STATE] = { + katexLoaded: true, + } + + await writer.addDependency({ + type: 'css', + src: 'katex/dist/katex.css', + }) + } + + const rawContent = tag.blocks.index(0).text() + + // TODO: Handle errors + const content = katex.renderToString(rawContent, { + throwOnError: false, + displayMode: true, + }) + + await writer.write(content) + }, + }, + + toc: { + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + + const tocItems = context.query(TOC) + + const stack: any[] = [] + + stack.push({ + content: '', + children: [], + previous: [], + }) + + while (stack.length > 1 || tocItems.length) { + let currentItem = stack.pop() + + const nextItem = tocItems.shift() + const nextItemLevel = nextItem?.type + ? tocDef.indexOf(nextItem.type) + : -1 + + if (nextItemLevel === stack.length) { + const index = stack + .concat([currentItem]) + .slice(1) + .map((item) => item.previous.length + 1) + .join('.') + + const renderedItem = ` +
  • + ${index} + ${ + currentItem.content + } + ${ + currentItem.children.length + ? `
      ${currentItem.children.join( + '\n' + )}
    ` + : '' + } + {{/if}} +
  • + ` + + currentItem = { + content: nextItem.content, + children: [], + previous: currentItem.previous.concat([ + renderedItem, + ]), + } + + stack.push(currentItem) + } else if (nextItemLevel > stack.length) { + stack.push(currentItem) + + while (nextItemLevel > stack.length) { + stack.push({ + content: '', + children: [], + previous: [], + }) + } + + stack.push({ + content: nextItem.content, + children: [], + previous: [], + }) + } else { + const index = stack + .concat([currentItem]) + .slice(1) + .map((item) => item.previous.length + 1) + .join('.') + + if (nextItem) { + tocItems.unshift(nextItem) + } + let parentItem = stack.pop() + + const renderedItem = ` +
  • + ${index} + ${ + currentItem.content + } + {{#if data.children.length}} +
      + ${ + currentItem.children.length + ? currentItem.children.join('\n') + : '' + } +
    + {{/if}} +
  • + ` + + parentItem = { + content: parentItem.content, + children: currentItem.previous.concat([ + renderedItem, + ]), + previous: parentItem.previous, + } + stack.push(parentItem) + } + } + + const rootItem = stack.pop() + const content = tag.blocks.index(0).text() + + await writer.write( + ` +

    + ${content} +

    +
      + ${rootItem.children.join('\n')} +
    + ` + ) + }, + }, + + blockquote: { + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + const content = tag.blocks.index(0).text() + + await writer.write( + ` +
    + ${content} +
    + ` + ) + }, + }, + + image: { + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + const link = tag.blocks.index(0).text() + const alt = tag.blocks.index(1).text() + + if (!link) { + return + } + + const isUrl = link.match(/(^|^http:|^https:)\/\//) + + if (isUrl) { + await writer.write(` +
    + ${alt} +
    + `) + } else { + await writer.copyFile( + link, + `images/${link.split(/[\\/]/).slice(-1)[0]}` + ) + + await writer.write(` +
    + ${alt} +
    + `) + } + }, + }, + + qrcode: { + tag: async (context: any, tag: any): Promise => { + const writer = context.plugins.get('writer') + const content = tag.blocks.index(0).text() + + const svg = await new Promise((resolve, reject) => { + qrcode.toString( + content, + { + type: 'svg', + margin: 1, + }, + (err, text) => { + if (err) { + reject(err) + } + + resolve(text) + } + ) + }) + + await writer.write(` +
    + ${svg} +
    + `) + }, + }, + } +} diff --git a/packages/posttext/src/web/builder.ts b/packages/posttext/src/web/builder.ts new file mode 100644 index 0000000..6442bef --- /dev/null +++ b/packages/posttext/src/web/builder.ts @@ -0,0 +1,41 @@ +import { Renderer, createRenderer } from './renderer' +import fs from 'fs-extra' +import { Parser, createParser } from '../parser' + +export interface Web { + build(): Promise +} + +export type BuilderFactory = () => Web + +export function createBuilderFactory(): BuilderFactory { + return createBuilder +} + +export function createBuilder(): Web { + const parser = createParser() + const renderer = createRenderer() + + return { + build: (): Promise => build(parser, renderer), + } +} + +const DEFAULT_INPUT_FILE = './docs/index.pt' + +async function build( + parser: Parser, + renderer: Renderer +): Promise { + const input = await fs.readFile(DEFAULT_INPUT_FILE, 'utf-8') + + const ast = parser.parse(input) + const { html } = await renderer.render(ast) + + await fs.writeFile('index.html', html) +} + +export default { + name: 'web', + factory: createBuilderFactory(), +}