18 Commits

Author SHA1 Message Date
4274045fca 0.2.0 2023-05-18 21:20:46 -07:00
95c15ee027 updated marked to v5; better baseurl handling 2023-05-18 21:20:39 -07:00
e184b6f8b4 0.1.21 2023-05-13 22:12:04 -07:00
e601f45836 update bin scripts to work as symlinks 2023-05-13 22:11:59 -07:00
8203bbe763 0.1.20 2023-05-13 20:13:50 -07:00
2bd98c788d export 2023-05-13 20:13:49 -07:00
96f468c4f8 0.1.19 2023-05-13 20:12:44 -07:00
c06c01823b add sync inline renderer 2023-05-13 20:12:39 -07:00
8afb5423e3 0.1.18 2023-05-12 15:07:47 -07:00
0f83a56299 cleanup 2023-05-12 15:07:43 -07:00
ae7b491107 0.1.17 2023-05-12 14:59:49 -07:00
8599460702 whitelist rdfa tags/attrs in html sanitizer 2023-05-12 14:59:45 -07:00
fb52d31090 0.1.16 2023-05-12 14:54:12 -07:00
859064f00b add rdfa for breadcrumbs 2023-05-12 14:54:05 -07:00
7803b495c2 0.1.15 2023-05-12 14:43:14 -07:00
f48c0c6b75 fixing bug 2023-05-12 14:42:57 -07:00
34cacb48bb 0.1.14 2023-05-12 14:35:54 -07:00
7d5d29dade add support for breadcrumb navs 2023-05-12 14:35:49 -07:00
10 changed files with 145 additions and 37 deletions

View File

@@ -1,2 +1,2 @@
#!/bin/sh #!/bin/sh
node $(dirname $0)/markdown2html.js $* node $(dirname "$(readlink -f "$0")")/markdown2html.js $*

View File

@@ -1,2 +1,2 @@
#!/bin/sh #!/bin/sh
node $(dirname $0)/strip-frontmatter.js $* node $(dirname "$(readlink -f "$0")")/strip-frontmatter.js $*

34
package-lock.json generated
View File

@@ -1,18 +1,18 @@
{ {
"name": "@doc-utils/markdown2html", "name": "@doc-utils/markdown2html",
"version": "0.1.13", "version": "0.2.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@doc-utils/markdown2html", "name": "@doc-utils/markdown2html",
"version": "0.1.13", "version": "0.2.0",
"dependencies": { "dependencies": {
"bytefield-svg": "^1.6.1", "bytefield-svg": "^1.6.1",
"dompurify": "^2.3.6", "dompurify": "^2.3.6",
"jsdom": "^20.0.1", "jsdom": "^20.0.1",
"katex": "^0.16.7", "katex": "^0.16.7",
"marked": "^4.1.1", "marked": "^5.0.2",
"nomnoml": "^1.5.2", "nomnoml": "^1.5.2",
"pikchr": "^0.0.5", "pikchr": "^0.0.5",
"prismjs": "^1.29.0", "prismjs": "^1.29.0",
@@ -28,7 +28,7 @@
"@types/jsdom": "^20.0.0", "@types/jsdom": "^20.0.0",
"@types/katex": "^0.16.0", "@types/katex": "^0.16.0",
"@types/luxon": "^3.1.0", "@types/luxon": "^3.1.0",
"@types/marked": "^4.0.3", "@types/marked": "^5.0.0",
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
"@types/prismjs": "^1.26.0", "@types/prismjs": "^1.26.0",
"@types/qrcode": "^1.5.0", "@types/qrcode": "^1.5.0",
@@ -86,9 +86,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/marked": { "node_modules/@types/marked": {
"version": "4.3.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.0.tgz", "resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.0.tgz",
"integrity": "sha512-zK4gSFMjgslsv5Lyvr3O1yCjgmnE4pr8jbG8qVn4QglMwtpvPCf4YT2Wma7Nk95OxUUJI8Z+kzdXohbM7mVpGw==", "integrity": "sha512-YcZe50jhltsCq7rc9MNZC/4QB/OnA2Pd6hrOSTOFajtabN+38slqgDDCeE/0F83SjkKBQcsZUj7VLWR0H5cKRA==",
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
@@ -954,14 +954,14 @@
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
}, },
"node_modules/marked": { "node_modules/marked": {
"version": "4.3.0", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-5.0.2.tgz",
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "integrity": "sha512-TXksm9GwqXCRNbFUZmMtqNLvy3K2cQHuWmyBDLOrY1e6i9UvZpOTJXoz7fBjYkJkaUFzV9hBFxMuZSyQt8R6KQ==",
"bin": { "bin": {
"marked": "bin/marked.js" "marked": "bin/marked.js"
}, },
"engines": { "engines": {
"node": ">= 12" "node": ">= 18"
} }
}, },
"node_modules/mime-db": { "node_modules/mime-db": {
@@ -2015,9 +2015,9 @@
"dev": true "dev": true
}, },
"@types/marked": { "@types/marked": {
"version": "4.3.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/marked/-/marked-4.3.0.tgz", "resolved": "https://registry.npmjs.org/@types/marked/-/marked-5.0.0.tgz",
"integrity": "sha512-zK4gSFMjgslsv5Lyvr3O1yCjgmnE4pr8jbG8qVn4QglMwtpvPCf4YT2Wma7Nk95OxUUJI8Z+kzdXohbM7mVpGw==", "integrity": "sha512-YcZe50jhltsCq7rc9MNZC/4QB/OnA2Pd6hrOSTOFajtabN+38slqgDDCeE/0F83SjkKBQcsZUj7VLWR0H5cKRA==",
"dev": true "dev": true
}, },
"@types/node": { "@types/node": {
@@ -2651,9 +2651,9 @@
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="
}, },
"marked": { "marked": {
"version": "4.3.0", "version": "5.0.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-5.0.2.tgz",
"integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==" "integrity": "sha512-TXksm9GwqXCRNbFUZmMtqNLvy3K2cQHuWmyBDLOrY1e6i9UvZpOTJXoz7fBjYkJkaUFzV9hBFxMuZSyQt8R6KQ=="
}, },
"mime-db": { "mime-db": {
"version": "1.52.0", "version": "1.52.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@doc-utils/markdown2html", "name": "@doc-utils/markdown2html",
"version": "0.1.13", "version": "0.2.0",
"publishConfig": { "publishConfig": {
"registry": "https://gitea.home.jbrumond.me/api/packages/doc-utils/npm/" "registry": "https://gitea.home.jbrumond.me/api/packages/doc-utils/npm/"
}, },
@@ -17,7 +17,7 @@
"@types/jsdom": "^20.0.0", "@types/jsdom": "^20.0.0",
"@types/katex": "^0.16.0", "@types/katex": "^0.16.0",
"@types/luxon": "^3.1.0", "@types/luxon": "^3.1.0",
"@types/marked": "^4.0.3", "@types/marked": "^5.0.0",
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
"@types/prismjs": "^1.26.0", "@types/prismjs": "^1.26.0",
"@types/qrcode": "^1.5.0", "@types/qrcode": "^1.5.0",
@@ -28,7 +28,7 @@
"dompurify": "^2.3.6", "dompurify": "^2.3.6",
"jsdom": "^20.0.1", "jsdom": "^20.0.1",
"katex": "^0.16.7", "katex": "^0.16.7",
"marked": "^4.1.1", "marked": "^5.0.2",
"nomnoml": "^1.5.2", "nomnoml": "^1.5.2",
"pikchr": "^0.0.5", "pikchr": "^0.0.5",
"prismjs": "^1.29.0", "prismjs": "^1.29.0",

11
src/base-url.ts Normal file
View File

@@ -0,0 +1,11 @@
import { marked } from 'marked';
import { MarkdownOptions } from './render';
export function base_url_walk_tokens(token: marked.Token, options: MarkdownOptions) {
if (options.base_url) {
if (token.type === 'link' || token.type === 'image') {
token.href = (new URL(token.href, options.base_url)).toString();
}
}
}

64
src/breadcrumb-nav.ts Normal file
View File

@@ -0,0 +1,64 @@
import { marked } from 'marked';
import { ParsedAttributes, parse_attributes } from './attrs';
import { MarkdownOptions } from './render';
// todo: deprecate this
export interface BreadcrumbNavToken extends marked.Tokens.Generic {
text: string;
attrs: ParsedAttributes;
items: marked.Token[][];
}
export function breadcrumb_nav_ext(renderer: marked.Renderer, opts: MarkdownOptions) : marked.TokenizerExtension & marked.RendererExtension {
return {
name: 'breadcrumb_nav',
level: 'block',
start: (src) => src.match(/^\/\/\//)?.index,
tokenizer(src, tokens) {
const rule = /^\/\/\/(\/*)([^\n]+)?(?:\n)((?:[^\/]|\/\/?(?!\/\1))+)\/\/\/\1/;
const match = rule.exec(src);
if (match) {
const token: BreadcrumbNavToken = {
type: 'breadcrumb_nav',
raw: match[0],
text: match[3],
attrs: parse_attributes(match[2] || ''),
tokens: [ ],
items: [ ],
};
const lines = match[3].trim().split('\n');
for (const line of lines) {
const tokens = this.lexer.inlineTokens(line, [ ]);
token.tokens.push(...tokens);
token.items.push(tokens);
}
return token;
}
},
renderer(token: BreadcrumbNavToken) {
return `<nav aria-label="breadcrumbs" ${token.attrs.html_attrs.join(' ')}>\n`
+ `\t<ol>\n`
+ '\t\t'
+ token.items.map((tokens, index) =>{
let item = '<li>\n';
if (index) {
item += '\t\t\t<span class="separator" aria-hidden="true">/</span>\n';
}
item += `\t\t\t${this.parser.parseInline(tokens, renderer)}\n`;
return item + '\t\t</li>';
}).join('\n\t\t')
+ '\n'
+ `\t</ol>\n`
+ `</nav>`;
}
};
}

View File

@@ -8,6 +8,6 @@ export function sanitize_html(html: string, custom_elements?: CustomElementHandl
const { window } = new JSDOM(''); const { window } = new JSDOM('');
const dom_purify = createDOMPurify(window as any as Window); const dom_purify = createDOMPurify(window as any as Window);
return dom_purify.sanitize(html, { return dom_purify.sanitize(html, {
CUSTOM_ELEMENT_HANDLING: custom_elements CUSTOM_ELEMENT_HANDLING: custom_elements,
}); });
} }

View File

@@ -1,3 +1,3 @@
export { process_frontmatter } from './frontmatter'; export { process_frontmatter } from './frontmatter';
export { render_markdown_to_html, MarkdownExtension, MarkdownOptions } from './render'; export { render_markdown_to_html, render_markdown_to_html_inline_sync, MarkdownExtension, MarkdownOptions } from './render';

View File

@@ -9,6 +9,8 @@ import { katex_block_ext, katex_inline_ext } from './katex';
import { footnote_list_ext, footnote_ref_ext } from './footnotes'; import { footnote_list_ext, footnote_ref_ext } from './footnotes';
import { description_list_ext } from './description-list'; import { description_list_ext } from './description-list';
import { resolve_async_bindings } from './async-steps'; import { resolve_async_bindings } from './async-steps';
import { breadcrumb_nav_ext } from './breadcrumb-nav';
import { base_url_walk_tokens } from './base-url';
export interface MarkdownOptions { export interface MarkdownOptions {
base_url?: string; base_url?: string;
@@ -24,6 +26,47 @@ export interface MarkdownExtension {
} }
export async function render_markdown_to_html(markdown: string, options: MarkdownOptions = { }) { export async function render_markdown_to_html(markdown: string, options: MarkdownOptions = { }) {
const marked_options: marked.MarkedOptions & { async: true } = {
async: true,
breaks: options.breaks || false,
renderer: create_renderer(options),
};
marked.use({
walkTokens(token) {
base_url_walk_tokens(token, options);
},
extensions: [
katex_block_ext(marked_options.renderer, options),
katex_inline_ext(marked_options.renderer, options),
footnote_ref_ext(marked_options.renderer, options),
footnote_list_ext(marked_options.renderer, options),
mark_ext(marked_options.renderer, options),
description_list_ext(marked_options.renderer, options),
section_ext(marked_options.renderer, options),
icon_ext(marked_options.renderer, options),
breadcrumb_nav_ext(marked_options.renderer, options),
...(options.extensions || [ ]).map((ext) => {
return ext(marked_options.renderer, options);
}),
],
tokenizer: {
url(src) {
// disable auto-linking; more can be added here to auto-link only sometimes
// see: https://github.com/markedjs/marked/issues/882#issuecomment-781628889
return null;
}
},
});
const unsafe_html = options.inline
? marked.parseInline(markdown, marked_options)
: await marked.parse(markdown, marked_options).then(resolve_async_bindings);
return sanitize_html(unsafe_html, options.custom_elements);
}
export function render_markdown_to_html_inline_sync(markdown: string, options: MarkdownOptions = { }) {
const marked_options: marked.MarkedOptions = { const marked_options: marked.MarkedOptions = {
baseUrl: options.base_url, baseUrl: options.base_url,
breaks: options.breaks || false, breaks: options.breaks || false,
@@ -40,6 +83,7 @@ export async function render_markdown_to_html(markdown: string, options: Markdow
description_list_ext(marked_options.renderer, options), description_list_ext(marked_options.renderer, options),
section_ext(marked_options.renderer, options), section_ext(marked_options.renderer, options),
icon_ext(marked_options.renderer, options), icon_ext(marked_options.renderer, options),
breadcrumb_nav_ext(marked_options.renderer, options),
...(options.extensions || [ ]).map((ext) => { ...(options.extensions || [ ]).map((ext) => {
return ext(marked_options.renderer, options); return ext(marked_options.renderer, options);
}), }),
@@ -53,17 +97,6 @@ export async function render_markdown_to_html(markdown: string, options: Markdow
}, },
}); });
const unsafe_html = options.inline const unsafe_html = marked.parseInline(markdown, marked_options);
? marked.parseInline(markdown, marked_options)
: await new Promise<string>((resolve, reject) => {
marked.parse(markdown, marked_options, (error, unsafe_html) => {
if (error) {
return reject(error);
}
resolve_async_bindings(unsafe_html).then(resolve, reject);
});
});
return sanitize_html(unsafe_html, options.custom_elements); return sanitize_html(unsafe_html, options.custom_elements);
} }

View File

@@ -197,7 +197,7 @@ function code(renderer: marked.Renderer, opts: MarkdownOptions) {
const arg_pattern = /^(?:[a-zA-Z0-9_:-]+|"(?:[^"\n]|(?<=\\)")*")/; const arg_pattern = /^(?:[a-zA-Z0-9_:-]+|"(?:[^"\n]|(?<=\\)")*")/;
function parse_code_args(text: string) { function parse_code_args(text = '') {
const args: string[] = [ ]; const args: string[] = [ ];
text = text.trim(); text = text.trim();