@traq-markdown-engine/commonmark-plugin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 traP
4
+ Copyright (c) 2026 東京工業大学デジタル創作同好会traP
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # commonmark-plugin: CommonMark and reusable extensions
2
+
3
+ CommonMark 0.31.2 と汎用 Markdown 拡張の文法・ノード契約・テキスト描画を所有します。
4
+
5
+ | crate | 責務 |
6
+ | ------------------------------- | --------------------------------------- |
7
+ | `markdown-commonmark-contracts` | CommonMark の型付きノード |
8
+ | `markdown-commonmark` | CommonMark の構文解析 |
9
+ | `markdown-commonmark-text` | CommonMark AST のテキスト描画 |
10
+ | `markdown-generic-contracts` | 数式、表、取り消し線、mark のノード契約 |
11
+ | `markdown-generic-syntax` | 汎用拡張の構文解析 |
12
+ | `markdown-generic-text` | 汎用拡張のテキスト描画 |
13
+
14
+ 各機能は独立した crate として利用できます。契約と構文実装は同じリポジトリで管理し、ノードを読むだけの処理が構文解析へ依存する必要はありません。
15
+
16
+ ## 開発
17
+
18
+ 共通の環境準備と検証は [ルートの CONTRIBUTING](../../../CONTRIBUTING.md) に従います。CommonMark の 652 仕様例の出典とライセンスは [root fixture](../../../tests/fixtures/README.md) に記載しています。
19
+
20
+ ## 境界
21
+
22
+ 依存先は [core](../../core/README.md) です。traP / traQ の文法・preset・アプリケーション方針は知りません。
23
+
24
+ traP 固有の拡張部品は [traq-plugin](../traq/README.md)、traQ 向けの構成と Wasm・TypeScript / Go bindings は [sdk](../../sdk/README.md)、HTML 描画は各パッケージの `typescript/renderer`、traQ の CSS は SDK パッケージにあります。
25
+
26
+ ## TypeScript / HTML rendering
27
+
28
+ TypeScript の実装もこのリポジトリの責務に合わせて配置しています。
29
+
30
+ | TypeScript package | 責務 |
31
+ | ----------------------------------------- | ---------------------------------------------------------------------- |
32
+ | `@traq-markdown-engine/core` | 共通 AST 型、HTML handler・Plugin・PresetBuilder、契約検証と生成の基盤 |
33
+ | `@traq-markdown-engine/commonmark-plugin` | CommonMark・汎用拡張の生成ノード型と HTML 描画 |
34
+ | `@traq-markdown-engine/traq-plugin` | traP の生成ノード型・参照・スタンプ等の HTML 描画 |
35
+ | `@traq-markdown-engine/sdk` | Wasm / Go / TypeScript 配布、traQ の描画構成・preview・CSS |
36
+
37
+ AST の共通形は core の `typescript/ast.ts` に一度だけ定義し、SDK の生成 bindings はそれを構文の union で特殊化します。構文の payload は Rust を正として生成し、commonmark-plugin と traq-plugin の `bun run generate:bindings` でそれぞれの契約 crate から再生成できます。
38
+
39
+ HTML API は `/renderer` サブパスです。traQ は `@traq-markdown-engine/sdk/renderer` の `messageRenderers`、CSS は `@traq-markdown-engine/sdk/index.css` を利用します。
40
+
41
+ ## Go contracts
42
+
43
+ The Go module is `github.com/uni-kakurenbo/traq-markdown-engine/packages/plugins/commonmark/go`. Payloads and node factories are generated from this repository's Rust contracts by `bun run generate:bindings`. The canonical tree and Wasm runtime belong to the core Go module.
@@ -0,0 +1,124 @@
1
+ export type Blockquote = Record<symbol, never>;
2
+ export type CodeBlock = {
3
+ fenced: boolean;
4
+ info: string;
5
+ literal: string;
6
+ };
7
+ export type Emphasis = Record<symbol, never>;
8
+ export type Hardbreak = Record<symbol, never>;
9
+ export type Heading = {
10
+ level: number;
11
+ };
12
+ export type HtmlBlock = {
13
+ literal: string;
14
+ };
15
+ export type HtmlInline = {
16
+ literal: string;
17
+ };
18
+ export type Image = {
19
+ destination: string;
20
+ title: string | null;
21
+ label_source: string;
22
+ };
23
+ export type InlineCode = {
24
+ literal: string;
25
+ };
26
+ export type Link = {
27
+ destination: string;
28
+ title: string | null;
29
+ form: LinkForm;
30
+ };
31
+ export type LinkForm = 'explicit' | 'autolink' | 'linkify';
32
+ export type List = {
33
+ ordered: boolean;
34
+ start: number;
35
+ tight: boolean;
36
+ };
37
+ export type ListItem = {
38
+ marker: string;
39
+ };
40
+ export type Paragraph = Record<symbol, never>;
41
+ export type Softbreak = Record<symbol, never>;
42
+ export type Strong = Record<symbol, never>;
43
+ export type Text = {
44
+ value: string;
45
+ };
46
+ export type ThematicBreak = {
47
+ marker: string;
48
+ };
49
+ export type NodeKind = {
50
+ kind: 'markdown_commonmark_contracts::nodes::Blockquote';
51
+ data: Blockquote;
52
+ } | {
53
+ kind: 'markdown_commonmark_contracts::nodes::CodeBlock';
54
+ data: CodeBlock;
55
+ } | {
56
+ kind: 'markdown_commonmark_contracts::nodes::Emphasis';
57
+ data: Emphasis;
58
+ } | {
59
+ kind: 'markdown_commonmark_contracts::nodes::Hardbreak';
60
+ data: Hardbreak;
61
+ } | {
62
+ kind: 'markdown_commonmark_contracts::nodes::Heading';
63
+ data: Heading;
64
+ } | {
65
+ kind: 'markdown_commonmark_contracts::nodes::HtmlBlock';
66
+ data: HtmlBlock;
67
+ } | {
68
+ kind: 'markdown_commonmark_contracts::nodes::HtmlInline';
69
+ data: HtmlInline;
70
+ } | {
71
+ kind: 'markdown_commonmark_contracts::nodes::Image';
72
+ data: Image;
73
+ } | {
74
+ kind: 'markdown_commonmark_contracts::nodes::InlineCode';
75
+ data: InlineCode;
76
+ } | {
77
+ kind: 'markdown_commonmark_contracts::nodes::Link';
78
+ data: Link;
79
+ } | {
80
+ kind: 'markdown_commonmark_contracts::nodes::List';
81
+ data: List;
82
+ } | {
83
+ kind: 'markdown_commonmark_contracts::nodes::ListItem';
84
+ data: ListItem;
85
+ } | {
86
+ kind: 'markdown_commonmark_contracts::nodes::Paragraph';
87
+ data: Paragraph;
88
+ } | {
89
+ kind: 'markdown_commonmark_contracts::nodes::Softbreak';
90
+ data: Softbreak;
91
+ } | {
92
+ kind: 'markdown_commonmark_contracts::nodes::Strong';
93
+ data: Strong;
94
+ } | {
95
+ kind: 'markdown_commonmark_contracts::nodes::Text';
96
+ data: Text;
97
+ } | {
98
+ kind: 'markdown_commonmark_contracts::nodes::ThematicBreak';
99
+ data: ThematicBreak;
100
+ };
101
+ export declare const names: Readonly<{
102
+ readonly Blockquote: 'markdown_commonmark_contracts::nodes::Blockquote';
103
+ readonly CodeBlock: 'markdown_commonmark_contracts::nodes::CodeBlock';
104
+ readonly Emphasis: 'markdown_commonmark_contracts::nodes::Emphasis';
105
+ readonly Hardbreak: 'markdown_commonmark_contracts::nodes::Hardbreak';
106
+ readonly Heading: 'markdown_commonmark_contracts::nodes::Heading';
107
+ readonly HtmlBlock: 'markdown_commonmark_contracts::nodes::HtmlBlock';
108
+ readonly HtmlInline: 'markdown_commonmark_contracts::nodes::HtmlInline';
109
+ readonly Image: 'markdown_commonmark_contracts::nodes::Image';
110
+ readonly InlineCode: 'markdown_commonmark_contracts::nodes::InlineCode';
111
+ readonly Link: 'markdown_commonmark_contracts::nodes::Link';
112
+ readonly List: 'markdown_commonmark_contracts::nodes::List';
113
+ readonly ListItem: 'markdown_commonmark_contracts::nodes::ListItem';
114
+ readonly Paragraph: 'markdown_commonmark_contracts::nodes::Paragraph';
115
+ readonly Softbreak: 'markdown_commonmark_contracts::nodes::Softbreak';
116
+ readonly Strong: 'markdown_commonmark_contracts::nodes::Strong';
117
+ readonly Text: 'markdown_commonmark_contracts::nodes::Text';
118
+ readonly ThematicBreak: 'markdown_commonmark_contracts::nodes::ThematicBreak';
119
+ }>;
120
+ export declare const nodes: ReadonlyMap<string, (data: unknown) => boolean>;
121
+ export declare function isKnownNode<T extends {
122
+ kind: string;
123
+ data: unknown;
124
+ }>(node: T): node is T & NodeKind;
@@ -0,0 +1,112 @@
1
+ // Generated from Rust node payload types. Do not edit.
2
+ import { boolean, fields, nullable, oneOf, string } from '@traq-markdown-engine/core/validation';
3
+ export const names = Object.freeze({
4
+ Blockquote: 'markdown_commonmark_contracts::nodes::Blockquote',
5
+ CodeBlock: 'markdown_commonmark_contracts::nodes::CodeBlock',
6
+ Emphasis: 'markdown_commonmark_contracts::nodes::Emphasis',
7
+ Hardbreak: 'markdown_commonmark_contracts::nodes::Hardbreak',
8
+ Heading: 'markdown_commonmark_contracts::nodes::Heading',
9
+ HtmlBlock: 'markdown_commonmark_contracts::nodes::HtmlBlock',
10
+ HtmlInline: 'markdown_commonmark_contracts::nodes::HtmlInline',
11
+ Image: 'markdown_commonmark_contracts::nodes::Image',
12
+ InlineCode: 'markdown_commonmark_contracts::nodes::InlineCode',
13
+ Link: 'markdown_commonmark_contracts::nodes::Link',
14
+ List: 'markdown_commonmark_contracts::nodes::List',
15
+ ListItem: 'markdown_commonmark_contracts::nodes::ListItem',
16
+ Paragraph: 'markdown_commonmark_contracts::nodes::Paragraph',
17
+ Softbreak: 'markdown_commonmark_contracts::nodes::Softbreak',
18
+ Strong: 'markdown_commonmark_contracts::nodes::Strong',
19
+ Text: 'markdown_commonmark_contracts::nodes::Text',
20
+ ThematicBreak: 'markdown_commonmark_contracts::nodes::ThematicBreak'
21
+ });
22
+ const validators = new Map([
23
+ [
24
+ 'markdown_commonmark_contracts::nodes::Blockquote',
25
+ value => fields(value, {}, {})
26
+ ],
27
+ [
28
+ 'markdown_commonmark_contracts::nodes::CodeBlock',
29
+ value => fields(value, { fenced: boolean, info: string, literal: string }, {})
30
+ ],
31
+ [
32
+ 'markdown_commonmark_contracts::nodes::Emphasis',
33
+ value => fields(value, {}, {})
34
+ ],
35
+ [
36
+ 'markdown_commonmark_contracts::nodes::Hardbreak',
37
+ value => fields(value, {}, {})
38
+ ],
39
+ [
40
+ 'markdown_commonmark_contracts::nodes::Heading',
41
+ value => fields(value, {
42
+ level: value => typeof value === 'number' &&
43
+ Number.isInteger(value) &&
44
+ value >= 0 &&
45
+ value <= 255
46
+ }, {})
47
+ ],
48
+ [
49
+ 'markdown_commonmark_contracts::nodes::HtmlBlock',
50
+ value => fields(value, { literal: string }, {})
51
+ ],
52
+ [
53
+ 'markdown_commonmark_contracts::nodes::HtmlInline',
54
+ value => fields(value, { literal: string }, {})
55
+ ],
56
+ [
57
+ 'markdown_commonmark_contracts::nodes::Image',
58
+ value => fields(value, { destination: string, label_source: string, title: nullable(string) }, {})
59
+ ],
60
+ [
61
+ 'markdown_commonmark_contracts::nodes::InlineCode',
62
+ value => fields(value, { literal: string }, {})
63
+ ],
64
+ [
65
+ 'markdown_commonmark_contracts::nodes::Link',
66
+ value => fields(value, {
67
+ destination: string,
68
+ form: oneOf('explicit', 'autolink', 'linkify'),
69
+ title: nullable(string)
70
+ }, {})
71
+ ],
72
+ [
73
+ 'markdown_commonmark_contracts::nodes::List',
74
+ value => fields(value, {
75
+ ordered: boolean,
76
+ start: value => typeof value === 'number' &&
77
+ Number.isInteger(value) &&
78
+ value >= 0 &&
79
+ value <= 4294967295,
80
+ tight: boolean
81
+ }, {})
82
+ ],
83
+ [
84
+ 'markdown_commonmark_contracts::nodes::ListItem',
85
+ value => fields(value, { marker: string }, {})
86
+ ],
87
+ [
88
+ 'markdown_commonmark_contracts::nodes::Paragraph',
89
+ value => fields(value, {}, {})
90
+ ],
91
+ [
92
+ 'markdown_commonmark_contracts::nodes::Softbreak',
93
+ value => fields(value, {}, {})
94
+ ],
95
+ [
96
+ 'markdown_commonmark_contracts::nodes::Strong',
97
+ value => fields(value, {}, {})
98
+ ],
99
+ [
100
+ 'markdown_commonmark_contracts::nodes::Text',
101
+ value => fields(value, { value: string }, {})
102
+ ],
103
+ [
104
+ 'markdown_commonmark_contracts::nodes::ThematicBreak',
105
+ value => fields(value, { marker: string }, {})
106
+ ]
107
+ ]);
108
+ export const nodes = new Map(validators);
109
+ // Check this payload only; children can still contain unknown nodes.
110
+ export function isKnownNode(node) {
111
+ return validators.get(node.kind)?.(node.data) ?? false;
112
+ }
@@ -0,0 +1,52 @@
1
+ export type MarkData = Record<symbol, never>;
2
+ export type BlockMathData = {
3
+ tex: string;
4
+ };
5
+ export type InlineMathData = {
6
+ tex: string;
7
+ };
8
+ export type StrikethroughData = Record<symbol, never>;
9
+ export type CellData = {
10
+ alignment: Alignment | null;
11
+ };
12
+ export type Alignment = 'left' | 'center' | 'right';
13
+ export type RowData = {
14
+ header: boolean;
15
+ };
16
+ export type TableData = Record<symbol, never>;
17
+ export type NodeKind = {
18
+ kind: 'markdown_generic_contracts::mark::MarkData';
19
+ data: MarkData;
20
+ } | {
21
+ kind: 'markdown_generic_contracts::math::BlockMathData';
22
+ data: BlockMathData;
23
+ } | {
24
+ kind: 'markdown_generic_contracts::math::InlineMathData';
25
+ data: InlineMathData;
26
+ } | {
27
+ kind: 'markdown_generic_contracts::strikethrough::StrikethroughData';
28
+ data: StrikethroughData;
29
+ } | {
30
+ kind: 'markdown_generic_contracts::table::CellData';
31
+ data: CellData;
32
+ } | {
33
+ kind: 'markdown_generic_contracts::table::RowData';
34
+ data: RowData;
35
+ } | {
36
+ kind: 'markdown_generic_contracts::table::TableData';
37
+ data: TableData;
38
+ };
39
+ export declare const names: Readonly<{
40
+ readonly Mark: 'markdown_generic_contracts::mark::MarkData';
41
+ readonly BlockMath: 'markdown_generic_contracts::math::BlockMathData';
42
+ readonly InlineMath: 'markdown_generic_contracts::math::InlineMathData';
43
+ readonly Strikethrough: 'markdown_generic_contracts::strikethrough::StrikethroughData';
44
+ readonly Cell: 'markdown_generic_contracts::table::CellData';
45
+ readonly Row: 'markdown_generic_contracts::table::RowData';
46
+ readonly Table: 'markdown_generic_contracts::table::TableData';
47
+ }>;
48
+ export declare const nodes: ReadonlyMap<string, (data: unknown) => boolean>;
49
+ export declare function isKnownNode<T extends {
50
+ kind: string;
51
+ data: unknown;
52
+ }>(node: T): node is T & NodeKind;
@@ -0,0 +1,46 @@
1
+ // Generated from Rust node payload types. Do not edit.
2
+ import { boolean, fields, nullable, oneOf, string } from '@traq-markdown-engine/core/validation';
3
+ export const names = Object.freeze({
4
+ Mark: 'markdown_generic_contracts::mark::MarkData',
5
+ BlockMath: 'markdown_generic_contracts::math::BlockMathData',
6
+ InlineMath: 'markdown_generic_contracts::math::InlineMathData',
7
+ Strikethrough: 'markdown_generic_contracts::strikethrough::StrikethroughData',
8
+ Cell: 'markdown_generic_contracts::table::CellData',
9
+ Row: 'markdown_generic_contracts::table::RowData',
10
+ Table: 'markdown_generic_contracts::table::TableData'
11
+ });
12
+ const validators = new Map([
13
+ [
14
+ 'markdown_generic_contracts::mark::MarkData',
15
+ value => fields(value, {}, {})
16
+ ],
17
+ [
18
+ 'markdown_generic_contracts::math::BlockMathData',
19
+ value => fields(value, { tex: string }, {})
20
+ ],
21
+ [
22
+ 'markdown_generic_contracts::math::InlineMathData',
23
+ value => fields(value, { tex: string }, {})
24
+ ],
25
+ [
26
+ 'markdown_generic_contracts::strikethrough::StrikethroughData',
27
+ value => fields(value, {}, {})
28
+ ],
29
+ [
30
+ 'markdown_generic_contracts::table::CellData',
31
+ value => fields(value, { alignment: nullable(oneOf('left', 'center', 'right')) }, {})
32
+ ],
33
+ [
34
+ 'markdown_generic_contracts::table::RowData',
35
+ value => fields(value, { header: boolean }, {})
36
+ ],
37
+ [
38
+ 'markdown_generic_contracts::table::TableData',
39
+ value => fields(value, {}, {})
40
+ ]
41
+ ]);
42
+ export const nodes = new Map(validators);
43
+ // Check this payload only; children can still contain unknown nodes.
44
+ export function isKnownNode(node) {
45
+ return validators.get(node.kind)?.(node.data) ?? false;
46
+ }
@@ -0,0 +1,5 @@
1
+ import type { Plugin } from '@traq-markdown-engine/core/renderer';
2
+ import type { Options } from './options.js';
3
+ type BlockOptions = Pick<Options, 'highlight'>;
4
+ export declare function registerBlockHandlers(result: Plugin, { highlight }: BlockOptions): void;
5
+ export {};
@@ -0,0 +1,76 @@
1
+ import { isKnownNode, names } from '@traq-markdown-engine/commonmark-plugin/nodes';
2
+ import { attributes, checked, escapeHtml } from '@traq-markdown-engine/core/html';
3
+ import { decodeHTMLStrict } from 'entities';
4
+ function tightList(node) {
5
+ return (!!node && isKnownNode(node) && node.kind === names.List && node.data.tight);
6
+ }
7
+ function codeLanguage(info) {
8
+ return info
9
+ .replace(/\\([!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~])|&(?:#[xX][0-9a-fA-F]+|#\d+|[a-zA-Z][a-zA-Z0-9]+);/g, (match, escaped) => escaped ?? decodeHTMLStrict(match))
10
+ .trim()
11
+ .split(/\s+/)[0];
12
+ }
13
+ export function registerBlockHandlers(result, { highlight }) {
14
+ result.on(names.Paragraph, checked(names.Paragraph, isKnownNode, (n, ctx) => {
15
+ const content = ctx.render(n.children);
16
+ return ctx.ancestors.at(-1)?.kind === names.ListItem &&
17
+ tightList(ctx.ancestors.at(-2))
18
+ ? content
19
+ : '<p>' + content + '</p>\n';
20
+ }));
21
+ result.on(names.Heading, checked(names.Heading, isKnownNode, (n, ctx) => '<h' +
22
+ n.data.level +
23
+ '>' +
24
+ ctx.render(n.children) +
25
+ '</h' +
26
+ n.data.level +
27
+ '>\n'));
28
+ result.on(names.Blockquote, checked(names.Blockquote, isKnownNode, (n, ctx) => '<blockquote>' +
29
+ (n.children?.length ? '\n' : '') +
30
+ ctx.render(n.children) +
31
+ '</blockquote>\n'));
32
+ result.on(names.List, checked(names.List, isKnownNode, (n, ctx) => {
33
+ const tag = n.data.ordered ? 'ol' : 'ul';
34
+ const attrs = n.data.ordered && n.data.start !== 1
35
+ ? attributes([['start', String(n.data.start)]])
36
+ : '';
37
+ return ('<' + tag + attrs + '>\n' + ctx.render(n.children) + '</' + tag + '>\n');
38
+ }));
39
+ result.on(names.ListItem, checked(names.ListItem, isKnownNode, (n, ctx) => {
40
+ const children = n.children ?? [];
41
+ const tight = tightList(ctx.ancestors.at(-1));
42
+ const content = children
43
+ .map((child, i) => {
44
+ const rendered = ctx.render([child]);
45
+ // A line break following a tight paragraph needs no leading whitespace.
46
+ const separator = tight &&
47
+ i > 0 &&
48
+ children[i - 1].kind === names.Paragraph &&
49
+ child.kind !== names.CodeBlock &&
50
+ !rendered.startsWith('<br>')
51
+ ? '\n'
52
+ : '';
53
+ return separator + rendered;
54
+ })
55
+ .join('');
56
+ return ('<li>' +
57
+ (children.length && !(tight && children[0].kind === names.Paragraph)
58
+ ? '\n'
59
+ : '') +
60
+ content +
61
+ '</li>\n');
62
+ }));
63
+ result.on(names.CodeBlock, checked(names.CodeBlock, isKnownNode, n => {
64
+ const language = n.data.fenced ? codeLanguage(n.data.info ?? '') : '';
65
+ const content = (n.data.fenced && highlight?.(n.data.literal, language)) ||
66
+ escapeHtml(n.data.literal);
67
+ if (content.startsWith('<pre'))
68
+ return content + '\n';
69
+ const attrs = language
70
+ ? attributes([['class', 'language-' + language]])
71
+ : '';
72
+ return '<pre><code' + attrs + '>' + content + '</code></pre>\n';
73
+ }));
74
+ result.on(names.ThematicBreak, checked(names.ThematicBreak, isKnownNode, () => '<hr>\n'));
75
+ result.on(names.HtmlBlock, checked(names.HtmlBlock, isKnownNode, n => '<p>' + escapeHtml(n.data.literal) + '</p>\n'));
76
+ }
@@ -0,0 +1,6 @@
1
+ import { Plugin } from '@traq-markdown-engine/core/renderer';
2
+ export interface Options {
3
+ /** Return trusted HTML. KaTeX is used by default. */
4
+ math?(tex: string, displayMode: boolean): string;
5
+ }
6
+ export declare function plugin({ math }?: Options): Plugin;
@@ -0,0 +1,44 @@
1
+ import { isKnownNode, names } from '@traq-markdown-engine/commonmark-plugin/generic/nodes';
2
+ import { Plugin as Declaration } from '@traq-markdown-engine/core/definitions';
3
+ import { attributes, checked } from '@traq-markdown-engine/core/html';
4
+ import { Plugin } from '@traq-markdown-engine/core/renderer';
5
+ import { math as defaultMath } from './math.js';
6
+ function table(node, ctx) {
7
+ const rows = (node.children ?? []).map(row => {
8
+ if (!isKnownNode(row) || row.kind !== names.Row)
9
+ throw new TypeError('Invalid table row');
10
+ return row;
11
+ });
12
+ const head = rows.filter(r => r.data.header), body = rows.filter(r => !r.data.header);
13
+ return ('<table>\n<thead>\n' +
14
+ head.map(node => row(node, ctx)).join('') +
15
+ '</thead>\n' +
16
+ (body.length
17
+ ? '<tbody>\n' + body.map(node => row(node, ctx)).join('') + '</tbody>\n'
18
+ : '') +
19
+ '</table>\n');
20
+ }
21
+ function row(node, ctx) {
22
+ const tag = node.data.header ? 'th' : 'td';
23
+ const cells = (node.children ?? [])
24
+ .map(cell => {
25
+ if (!isKnownNode(cell) || cell.kind !== names.Cell)
26
+ throw new TypeError('Invalid table cell');
27
+ const attrs = cell.data.alignment
28
+ ? attributes([['style', 'text-align:' + cell.data.alignment]])
29
+ : '';
30
+ return ('<' + tag + attrs + '>' + ctx.render(cell.children) + '</' + tag + '>\n');
31
+ })
32
+ .join('');
33
+ return '<tr>\n' + cells + '</tr>\n';
34
+ }
35
+ const declaration = Declaration.group('generic').new('presentation');
36
+ export function plugin({ math = defaultMath } = {}) {
37
+ const result = new Plugin(declaration);
38
+ result.on(names.Mark, checked(names.Mark, isKnownNode, (n, ctx) => '<mark>' + ctx.render(n.children) + '</mark>'));
39
+ result.on(names.Strikethrough, checked(names.Strikethrough, isKnownNode, (n, ctx) => '<s>' + ctx.render(n.children) + '</s>'));
40
+ result.on(names.Table, checked(names.Table, isKnownNode, table));
41
+ result.on(names.InlineMath, checked(names.InlineMath, isKnownNode, node => math(node.data.tex, false)));
42
+ result.on(names.BlockMath, checked(names.BlockMath, isKnownNode, node => math(node.data.tex, true)));
43
+ return result;
44
+ }
@@ -0,0 +1,4 @@
1
+ export declare function math(tex: string, displayMode: boolean, options?: {
2
+ maxSize?: number;
3
+ macros?: Record<string, string>;
4
+ }): string;
@@ -0,0 +1,24 @@
1
+ import { escapeHtml } from '@traq-markdown-engine/core/html';
2
+ import katex from 'katex';
3
+ export function math(tex, displayMode, options = {}) {
4
+ try {
5
+ const html = katex.renderToString(tex, {
6
+ displayMode,
7
+ output: 'html',
8
+ maxSize: 100,
9
+ ...options,
10
+ strict: code => (code === 'unicodeTextInMathMode' ? 'ignore' : 'warn')
11
+ });
12
+ return displayMode ? `<p class="katex-block is-scroll">${html}</p>\n` : html;
13
+ }
14
+ catch (error) {
15
+ if (!(error instanceof katex.ParseError))
16
+ throw error;
17
+ const tag = displayMode ? 'p' : 'span';
18
+ const classes = displayMode
19
+ ? 'katex-block katex-error is-scroll'
20
+ : 'katex-error';
21
+ return (`<${tag} class="${classes}" title="${escapeHtml(String(error))}">${escapeHtml(tex)}</${tag}>` +
22
+ (displayMode ? '\n' : ''));
23
+ }
24
+ }
@@ -0,0 +1 @@
1
+ export declare const createHighlightFunc: (preClass: string, withCaption?: boolean, useSubsetForAuto?: boolean) => (code: string, lang: string) => string;
@@ -0,0 +1,29 @@
1
+ import { escapeHtml } from '@traq-markdown-engine/core/html';
2
+ import hljs from 'highlight.js';
3
+ import defaultSubset from './languages.js';
4
+ const noHighlightRe = /^(no-?highlight|plain|text)$/i;
5
+ export const createHighlightFunc = (preClass, withCaption = true, useSubsetForAuto = true) => (code, lang) => {
6
+ let langName;
7
+ let citeTag = '';
8
+ if (withCaption) {
9
+ const [_langName, langCaption] = lang.split(':');
10
+ langName = _langName ?? '';
11
+ if (langCaption) {
12
+ citeTag = `<cite>${escapeHtml(langCaption)}</cite>`;
13
+ }
14
+ }
15
+ else {
16
+ langName = lang;
17
+ }
18
+ if (hljs.getLanguage(langName)) {
19
+ const result = hljs.highlight(code, { language: langName });
20
+ return `<pre class="${preClass}">${citeTag}<code class="lang-${result.language}">${result.value}</code></pre>`;
21
+ }
22
+ else if (noHighlightRe.test(langName)) {
23
+ return `<pre class="${preClass}">${citeTag}<code>${escapeHtml(code)}</code></pre>`;
24
+ }
25
+ else {
26
+ const result = hljs.highlightAuto(code, useSubsetForAuto ? defaultSubset : undefined);
27
+ return `<pre class="${preClass}">${citeTag}<code class="lang-${result.language}">${result.value}</code></pre>`;
28
+ }
29
+ };
@@ -0,0 +1,10 @@
1
+ import { names } from '@traq-markdown-engine/commonmark-plugin/nodes';
2
+ import { Plugin } from '@traq-markdown-engine/core/renderer';
3
+ import type { Options } from './options.js';
4
+ export type { Options } from './options.js';
5
+ export { names as nodes };
6
+ export declare function plugin({ validateLink, validateImage, breaks, highlight, linkAttributes }?: Options): Plugin;
7
+ export declare const html: Readonly<{
8
+ plugin: typeof plugin;
9
+ }>;
10
+ export declare function preset(options?: Options): import("@traq-markdown-engine/core/renderer").Preset;
@@ -0,0 +1,24 @@
1
+ import { names } from '@traq-markdown-engine/commonmark-plugin/nodes';
2
+ import { Plugin as Declaration } from '@traq-markdown-engine/core/definitions';
3
+ import { Plugin } from '@traq-markdown-engine/core/renderer';
4
+ import { PresetBuilder } from '@traq-markdown-engine/core/renderer';
5
+ import { registerBlockHandlers } from './block.js';
6
+ import { registerInlineHandlers } from './inline.js';
7
+ import { validateLink as defaultPolicy } from './policy.js';
8
+ const declaration = Declaration.group('commonmark').new('core');
9
+ export { names as nodes };
10
+ export function plugin({ validateLink = defaultPolicy, validateImage = validateLink, breaks = false, highlight, linkAttributes = {} } = {}) {
11
+ const result = new Plugin(declaration);
12
+ registerInlineHandlers(result, {
13
+ validateLink,
14
+ validateImage,
15
+ breaks,
16
+ linkAttributes
17
+ });
18
+ registerBlockHandlers(result, { highlight });
19
+ return result;
20
+ }
21
+ export const html = Object.freeze({ plugin });
22
+ export function preset(options) {
23
+ return new PresetBuilder().add(plugin(options)).build();
24
+ }
@@ -0,0 +1,5 @@
1
+ import type { Plugin } from '@traq-markdown-engine/core/renderer';
2
+ import type { Options } from './options.js';
3
+ type InlineOptions = Required<Pick<Options, 'validateLink' | 'validateImage' | 'breaks' | 'linkAttributes'>>;
4
+ export declare function registerInlineHandlers(result: Plugin, { validateLink, validateImage, breaks, linkAttributes }: InlineOptions): void;
5
+ export {};
@@ -0,0 +1,61 @@
1
+ import { isKnownNode, names } from '@traq-markdown-engine/commonmark-plugin/nodes';
2
+ import { attributes, checked, escapeHtml } from '@traq-markdown-engine/core/html';
3
+ function imageText(nodes = [], source) {
4
+ return nodes
5
+ .map(node => {
6
+ if (!isKnownNode(node))
7
+ return new TextDecoder().decode(new TextEncoder()
8
+ .encode(source)
9
+ .subarray(node.span.start, node.span.end));
10
+ switch (node.kind) {
11
+ case names.Text:
12
+ return node.data.value;
13
+ case names.HtmlInline:
14
+ return node.data.literal;
15
+ case names.Softbreak:
16
+ case names.Hardbreak:
17
+ return '\n';
18
+ case names.InlineCode:
19
+ return node.data.literal;
20
+ default:
21
+ return imageText(node.children, source);
22
+ }
23
+ })
24
+ .join('');
25
+ }
26
+ function titleAttribute(title) {
27
+ return title === null ? '' : attributes([['title', title]]);
28
+ }
29
+ export function registerInlineHandlers(result, { validateLink, validateImage, breaks, linkAttributes }) {
30
+ const linkAttrs = attributes(Object.entries(linkAttributes));
31
+ result.on(names.Text, checked(names.Text, isKnownNode, n => escapeHtml(n.data.value)));
32
+ result.on(names.InlineCode, checked(names.InlineCode, isKnownNode, n => '<code>' + escapeHtml(n.data.literal) + '</code>'));
33
+ result.on(names.Softbreak, checked(names.Softbreak, isKnownNode, () => (breaks ? '<br>\n' : '\n')));
34
+ result.on(names.Hardbreak, checked(names.Hardbreak, isKnownNode, () => '<br>\n'));
35
+ result.on(names.Emphasis, checked(names.Emphasis, isKnownNode, (n, ctx) => '<em>' + ctx.render(n.children) + '</em>'));
36
+ result.on(names.Strong, checked(names.Strong, isKnownNode, (n, ctx) => '<strong>' + ctx.render(n.children) + '</strong>'));
37
+ result.on(names.Link, checked(names.Link, isKnownNode, (n, ctx) => {
38
+ const content = ctx.render(n.children);
39
+ if (!validateLink(n.data.destination))
40
+ return content;
41
+ return ('<a' +
42
+ attributes([['href', n.data.destination]]) +
43
+ linkAttrs +
44
+ titleAttribute(n.data.title) +
45
+ '>' +
46
+ content +
47
+ '</a>');
48
+ }));
49
+ result.on(names.Image, checked(names.Image, isKnownNode, (n, ctx) => {
50
+ if (!validateImage(n.data.destination))
51
+ return ctx.fallback(n);
52
+ return ('<img' +
53
+ attributes([
54
+ ['src', n.data.destination],
55
+ ['alt', imageText(n.children, ctx.source)]
56
+ ]) +
57
+ titleAttribute(n.data.title) +
58
+ '>');
59
+ }));
60
+ result.on(names.HtmlInline, checked(names.HtmlInline, isKnownNode, n => escapeHtml(n.data.literal)));
61
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: string[];
2
+ export default _default;
@@ -0,0 +1,82 @@
1
+ export default [
2
+ 'actionscript',
3
+ 'awk',
4
+ 'bash',
5
+ 'basic',
6
+ 'bnf',
7
+ 'csharp',
8
+ 'h',
9
+ 'cpp',
10
+ 'cmake',
11
+ 'coq',
12
+ 'css',
13
+ 'clojure',
14
+ 'coffeescript',
15
+ 'crystal',
16
+ 'd',
17
+ 'dart',
18
+ 'delphi',
19
+ 'diff',
20
+ 'django',
21
+ 'dockerfile',
22
+ 'elixir',
23
+ 'elm',
24
+ 'fsharp',
25
+ 'fortran',
26
+ 'go',
27
+ 'gradle',
28
+ 'groovy',
29
+ 'xml',
30
+ 'http',
31
+ 'haml',
32
+ 'handlebars',
33
+ 'haxe',
34
+ 'ini',
35
+ 'json',
36
+ 'java',
37
+ 'javascript',
38
+ 'kotlin',
39
+ 'tex',
40
+ 'less',
41
+ 'lisp',
42
+ 'livescript',
43
+ 'lua',
44
+ 'makefile',
45
+ 'markdown',
46
+ 'mathematica',
47
+ 'matlab',
48
+ 'nginx',
49
+ 'nimrod',
50
+ 'ocaml',
51
+ 'objectivec',
52
+ 'glsl',
53
+ 'graphql',
54
+ 'php',
55
+ 'perl',
56
+ 'plaintext',
57
+ 'pgsql',
58
+ 'powershell',
59
+ 'processing',
60
+ 'prolog',
61
+ 'protobuf',
62
+ 'python',
63
+ 'r',
64
+ 'ruby',
65
+ 'scss',
66
+ 'sql',
67
+ 'scheme',
68
+ 'shell',
69
+ 'stylus',
70
+ 'swift',
71
+ 'twig',
72
+ 'typescript',
73
+ 'vbnet',
74
+ 'vbscript',
75
+ 'verilog',
76
+ 'vim',
77
+ 'x86asm',
78
+ 'xquery',
79
+ 'yaml',
80
+ 'wasm',
81
+ 'zephir'
82
+ ];
@@ -0,0 +1,8 @@
1
+ export interface Options {
2
+ validateLink?(destination: string): boolean;
3
+ validateImage?(destination: string): boolean;
4
+ breaks?: boolean;
5
+ /** Return highlighted HTML, or an empty string to use escaped code. */
6
+ highlight?(code: string, language: string): string;
7
+ linkAttributes?: Readonly<Record<string, string>>;
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function validateLink(destination: string): boolean;
@@ -0,0 +1,10 @@
1
+ // Conservative default for standalone rendering. Application adapters supply
2
+ // their existing policy so integrating the shared parser does not change it.
3
+ export function validateLink(destination) {
4
+ try {
5
+ return ['http:', 'https:', 'mailto:', 'ftp:'].includes(new URL(destination, 'https://markdown.invalid').protocol);
6
+ }
7
+ catch {
8
+ return false;
9
+ }
10
+ }
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@traq-markdown-engine/commonmark-plugin",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "commonmark Markdown contracts and HTML rendering.",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "registry": "https://registry.npmjs.org"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/uni-kakurenbo/traq-markdown-engine.git",
14
+ "directory": "packages/plugins/commonmark"
15
+ },
16
+ "exports": {
17
+ "./nodes": {
18
+ "types": "./dist/generated/commonmark.d.ts",
19
+ "import": "./dist/generated/commonmark.js"
20
+ },
21
+ "./generic/nodes": {
22
+ "types": "./dist/generated/generic.d.ts",
23
+ "import": "./dist/generated/generic.js"
24
+ },
25
+ "./renderer": {
26
+ "types": "./dist/renderer/index.d.ts",
27
+ "import": "./dist/renderer/index.js"
28
+ },
29
+ "./generic/renderer": {
30
+ "types": "./dist/renderer/extensions/index.d.ts",
31
+ "import": "./dist/renderer/extensions/index.js"
32
+ },
33
+ "./generic/math": {
34
+ "types": "./dist/renderer/extensions/math.d.ts",
35
+ "import": "./dist/renderer/extensions/math.js"
36
+ },
37
+ "./highlight": {
38
+ "types": "./dist/renderer/highlight.d.ts",
39
+ "import": "./dist/renderer/highlight.js"
40
+ },
41
+ "./policy": {
42
+ "types": "./dist/renderer/policy.d.ts",
43
+ "import": "./dist/renderer/policy.js"
44
+ }
45
+ },
46
+ "files": [
47
+ "dist",
48
+ "LICENSE"
49
+ ],
50
+ "sideEffects": false,
51
+ "engines": {
52
+ "node": ">=24"
53
+ },
54
+ "scripts": {
55
+ "build": "bun run ../../../scripts/tsc.ts -p typescript/tsconfig.build.json",
56
+ "generate:bindings": "bun run ../../../scripts/generate-bindings.ts commonmark-plugin",
57
+ "typecheck": "bun run ../../../scripts/tsc.ts -p typescript/tsconfig.json"
58
+ },
59
+ "peerDependencies": {
60
+ "@traq-markdown-engine/core": "0.1.0"
61
+ },
62
+ "devDependencies": {
63
+ "@traq-markdown-engine/core": "workspace:*",
64
+ "@types/katex": "0.16.8"
65
+ },
66
+ "dependencies": {
67
+ "entities": "8.0.0",
68
+ "highlight.js": "11.12.0",
69
+ "katex": "0.18.5"
70
+ }
71
+ }