@posttext/modules
Version:
The official modules for Posttext
891 lines (803 loc) • 20 kB
text/typescript
/* 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,
RegistryOptions,
Command,
} from '@posttext/registry'
import { extractParagraphs } from '@posttext/ptlib'
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 tagResolvers = (
_options: RegistryOptions
): Record<string, Resolver> => {
return {
__root__: {
resolve: async function* (): AsyncGenerator<
Command,
void,
any
> {
const content = yield {
name: 'getBlockInlines',
transform: {
...extractParagraphs,
},
}
yield {
name: 'html',
template: `{{{ data.content }}}`,
data: {
content,
},
}
},
},
posttext: {
resolve: async function* (): AsyncGenerator<
Command,
void,
any
> {
const attrs: Record<string, string> = yield {
name: 'getAttrs',
}
const title: string | undefined = attrs.title
yield {
name: 'metadata',
metadata: {
title,
},
}
},
},
comment: {
resolve: async function* (): AsyncGenerator<
Command,
void,
any
> {
/* pass */
},
},
title: {
resolve: async function* (): AsyncGenerator<
Command,
any,
any
> {
const content: string | undefined = yield {
name: 'getBlock',
index: 0,
}
yield {
name: 'html',
template: '<h1>{{{ data.content }}}</h1>',
data: {
content: content ?? '',
},
}
},
},
section: {
preload: async function* (): AsyncGenerator<
Command,
void,
any
> {
const content: string | undefined = yield {
name: 'getBlock',
index: 0,
}
yield {
name: 'send',
topic: TOC,
data: {
tagName: 'section',
content,
},
}
},
resolve: async function* (): AsyncGenerator<
Command,
void,
any
> {
const content: string | undefined = yield {
name: 'getBlock',
index: 0,
}
yield {
name: 'html',
template: '<h2>{{{ data.content }}}</h2>',
data: {
content: content ?? '',
},
}
},
},
subsection: {
preload: async function* (): AsyncGenerator<
Command,
void,
any
> {
const content: string | undefined = yield {
name: 'getBlock',
index: 0,
}
yield {
name: 'send',
topic: TOC,
data: {
tagName: 'subsection',
content,
},
}
},
resolve: async function* (): AsyncGenerator<
Command,
void,
any
> {
const content: string | undefined = yield {
name: 'getBlock',
index: 0,
}
yield {
name: 'html',
template: '<h3>{{{ data.content }}}</h3>',
data: {
content: content ?? '',
},
}
},
},
subsubsection: {
preload: async function* (): AsyncGenerator<
Command,
void,
any
> {
const content: string | undefined = yield {
name: 'getBlock',
index: 0,
}
yield {
name: 'send',
topic: TOC,
data: {
tagName: 'subsubsection',
content,
},
}
},
resolve: async function* (): AsyncGenerator<
Command,
void,
any
> {
const content: string | undefined = yield {
name: 'getBlock',
index: 0,
}
yield {
name: 'html',
template: '<h4>{{{ data.content }}}</h4>',
data: {
content: content ?? '',
},
}
},
},
bold: {
resolve: async function* (): AsyncGenerator<
Command,
void,
any
> {
const content: string | undefined = yield {
name: 'getBlock',
index: 0,
}
yield {
name: 'html',
template: '<b>{{{ data.content }}}</b>',
type: 'inline',
data: {
content: content?.replace(/\s\s\n/g, '<br>') ?? '',
},
}
},
},
italic: {
resolve: async function* (): AsyncGenerator<
Command,
void,
any
> {
const content: string | undefined = yield {
name: 'getBlock',
index: 0,
}
yield {
name: 'html',
template: '<i>{{{ data.content }}}</i>',
type: 'inline',
data: {
content: content?.replace(/\s\s\n/g, '<br>') ?? '',
},
}
},
},
underline: {
resolve: async function* (): AsyncGenerator<
Command,
void,
any
> {
const content: string | undefined = yield {
name: 'getBlock',
index: 0,
}
yield {
name: 'html',
template: '<u>{{{ data.content }}}<u>',
type: 'inline',
data: {
content: content?.replace(/\s\s\n/g, '<br>') ?? '',
},
}
},
},
list: {
resolve: async function* (): AsyncGenerator<
Command,
void,
any
> {
const content: string | undefined = yield {
name: 'getBlock',
index: 0,
}
yield {
name: 'html',
template: '<ul>{{{ data.content }}}</ul>',
data: {
content: content ?? '',
},
}
},
},
item: {
resolve: async function* (): AsyncGenerator<
Command,
void,
any
> {
const content: string | undefined = yield {
name: 'getBlock',
index: 0,
}
yield {
name: 'html',
template: '<li>{{{ data.content }}}</li>',
data: {
content: content?.replace(/\s\s\n/g, '<br>') ?? '',
},
}
},
},
code: {
preload: async function* (): AsyncGenerator<
Command,
any,
any
> {
const state: Record<string, any> = yield {
name: 'getState',
symbol: CODE_BLOCK_STATE,
}
if (!state.languagesLoaded) {
state.languagesLoaded = true
loadLanguages()
yield {
name: 'addDeps',
deps: [
{
type: 'css',
src: 'prismjs/themes/prism.css',
},
],
}
}
},
resolve: async function* (): AsyncGenerator<
Command,
void,
any
> {
const params: string[] = yield {
name: 'getParams',
}
const language: string =
params[0] &&
Object.keys(Prism.languages).indexOf(params[0])
? params[0]
: 'text'
const BEGIN_NEWLINE = /^\r?\n/
const END_NEWLINE = /\r?\n[\t ]+$/
const rawTextContent: string | undefined = yield {
name: 'textContent',
index: 0,
}
const textContent: string = rawTextContent
? stripIndent(rawTextContent)
.replace(BEGIN_NEWLINE, '')
.replace(END_NEWLINE, '')
: ''
const code: string =
language !== 'text'
? Prism.highlight(
textContent,
Prism.languages[language],
language
)
: textContent
yield {
name: 'html',
template:
'<pre class="std_code"><code class="language-{{ data.language }}">{{{ data.code }}}</code></pre>',
data: {
language,
code,
},
}
},
},
"code'": {
preload: async function* (): AsyncGenerator<
Command,
any,
any
> {
const state: Record<string, any> = yield {
name: 'getState',
symbol: CODE_BLOCK_STATE,
}
if (!state.languagesLoaded) {
state.languagesLoaded = true
loadLanguages()
yield {
name: 'addDeps',
deps: [
{
type: 'css',
src: 'prismjs/themes/prism.css',
},
],
}
}
},
resolve: async function* (): AsyncGenerator<
Command,
void,
any
> {
const params: string[] = yield {
name: 'getParams',
}
const language: string =
params[0] &&
Object.keys(Prism.languages).indexOf(params[0])
? params[0]
: 'text'
const BEGIN_NEWLINE = /^\r?\n/
const END_NEWLINE = /\r?\n[\t ]+$/
const rawTextContent: string | undefined = yield {
name: 'textContent',
index: 0,
}
const textContent: string = rawTextContent
? stripIndent(rawTextContent)
.replace(BEGIN_NEWLINE, '')
.replace(END_NEWLINE, '')
: ''
const code: string =
language !== 'text'
? Prism.highlight(
textContent,
Prism.languages[language],
language
)
: textContent
yield {
name: 'html',
template:
'<code class="std_code language-{{ data.language }}">{{{ data.code }}}</code>',
type: 'inline',
data: {
language,
code,
},
}
},
},
katex: {
preload: async function* (): AsyncGenerator<
Command,
any,
any
> {
const state: Record<string, any> = yield {
name: 'getState',
symbol: KATEX_STATE,
}
if (!state.katexLoaded) {
state.katexLoaded = true
yield {
name: 'addDeps',
deps: [
{
type: 'css',
src: 'katex/dist/katex.css',
},
],
}
}
},
resolve: async function* (): AsyncGenerator<
Command,
void,
any
> {
const rawTextContent: string | undefined = yield {
name: 'textContent',
}
const content = katex.renderToString(rawTextContent, {
throwOnError: false,
displayMode: true,
})
yield {
name: 'html',
template: `{{{ data.content }}}`,
data: {
content,
},
}
},
},
"katex'": {
preload: async function* (): AsyncGenerator<
Command,
any,
any
> {
const state: Record<string, any> = yield {
name: 'getState',
symbol: KATEX_STATE,
}
if (!state.katexLoaded) {
state.katexLoaded = true
yield {
name: 'addDeps',
deps: [
{
type: 'css',
src: 'katex/dist/katex.css',
},
],
}
}
},
resolve: async function* (): AsyncGenerator<
Command,
void,
any
> {
const rawTextContent: string | undefined = yield {
name: 'textContent',
}
const content = katex.renderToString(rawTextContent, {
throwOnError: false,
})
yield {
name: 'html',
template: `{{{ data.content }}}`,
type: 'inline',
data: {
content,
},
}
},
},
toc: {
resolve: async function* (): AsyncGenerator<
Command,
void,
any
> {
const tocItems: any[] = yield {
name: 'receive',
topic: 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?.tagName
? tocDef.indexOf(nextItem.tagName)
: -1
if (nextItemLevel === stack.length) {
const counter = stack
.concat([currentItem])
.slice(1)
.map((item) => item.previous.length + 1)
.join('.')
const renderedItem = yield {
name: 'html',
template: `
<li class="std_toc__item">
<span class="std_toc__number">{{ data.counter }}</span>
<span class="std_toc__text">{{ data.content }}</span>
{{#if data.children.length}}
<ul class="std_toc__list">
{{#each data.children}}
{{{ this }}}
{{/each}}
</ul>
{{/if}}
</li>
`,
emit: false,
data: {
content: currentItem.content,
children: currentItem.children,
counter,
},
}
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 counter = stack
.concat([currentItem])
.slice(1)
.map((item) => item.previous.length + 1)
.join('.')
if (nextItem) {
tocItems.unshift(nextItem)
}
let parentItem = stack.pop()
const renderedItem = yield {
name: 'html',
template: `
<li class="std_toc__item">
<span class="std_toc__number">{{ data.counter }}</span>
<span class="std_toc__text">{{ data.content }}</span>
{{#if data.children.length}}
<ul class="std_toc__list">
{{#each data.children}}
{{{ this }}}
{{/each}}
</ul>
{{/if}}
</li>
`,
emit: false,
data: {
content: currentItem.content,
children: currentItem.children,
counter,
},
}
parentItem = {
content: parentItem.content,
children: currentItem.previous.concat([
renderedItem,
]),
previous: parentItem.previous,
}
stack.push(parentItem)
}
}
const rootItem = stack.pop()
const content: string | undefined = yield {
name: 'getBlock',
index: 0,
}
yield {
name: 'html',
template: `
<h2 class="std_toc__title">
{{{ data.content }}}
</h2>
<ul class="std_toc__list">
{{#each data.items}}
{{{ this }}}
{{/each}}
</ul>
`,
data: {
items: rootItem.children,
content,
},
}
},
},
blockquote: {
resolve: async function* (): AsyncGenerator<
Command,
any,
any
> {
const content = yield {
name: 'getBlockInlines',
transform: {
...extractParagraphs,
},
}
yield {
name: 'html',
template: `
<blockquote class="std_blockquote">
{{{ data.content }}}
</blockquote>
`,
data: {
content,
},
}
},
},
image: {
resolve: async function* (): AsyncGenerator<
Command,
any,
any
> {
const link: string | undefined = yield {
name: 'textContent',
index: 0,
} ?? ''
const alt: string | undefined = yield {
name: 'textContent',
index: 1,
}
if (!link) {
return
}
const isUrl = link.match(/(^|^http:|^https:)\/\//)
if (isUrl) {
yield {
name: 'html',
template: `
<div class="std_image">
<img src="{{ data.link }}"{{#if data.alt}} alt="{{ data.alt }}"{{/if}}>
</div>
`,
data: {
link,
alt,
},
}
} else {
const dest = yield {
name: 'copyFile',
src: link,
dest: `images/${link.split(/[\\/]/).slice(-1)[0]}`,
}
yield {
name: 'html',
template: `
<div class="std_image">
<img src="{{ data.link }}"{{#if data.alt}} alt="{{ data.alt }}"{{/if}}>
</div>
`,
data: {
link: dest,
alt,
},
}
}
},
},
qrcode: {
resolve: async function* (): AsyncGenerator<
Command,
any,
any
> {
const content: string | undefined = yield {
name: 'textContent',
index: 0,
} ?? ''
const svg = await new Promise((resolve, reject) => {
qrcode.toString(
content,
{
type: 'svg',
margin: 1,
},
(err, text) => {
if (err) {
reject(err)
}
resolve(text)
}
)
})
yield {
name: 'html',
template:
'<div class="std_qrcode">{{{ data.content }}}</div>',
data: {
content: svg,
},
}
},
},
box: {
resolve: async function* (): AsyncGenerator<
Command,
void,
any
> {
const content = yield {
name: 'getBlockInlines',
transform: {
...extractParagraphs,
},
}
yield {
name: 'html',
template: `
<div class="std_box">
{{{ data.content }}}
</div>
`,
data: {
content,
},
}
},
},
}
}