@memberjunction/markdown-core 0.0.0 → 5.45.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/dist/engine/markdown-engine.d.ts +109 -0
- package/dist/engine/markdown-engine.d.ts.map +1 -0
- package/dist/engine/markdown-engine.js +292 -0
- package/dist/engine/markdown-engine.js.map +1 -0
- package/dist/extensions/collapsible-headings.extension.d.ts +62 -0
- package/dist/extensions/collapsible-headings.extension.d.ts.map +1 -0
- package/dist/extensions/collapsible-headings.extension.js +174 -0
- package/dist/extensions/collapsible-headings.extension.js.map +1 -0
- package/dist/extensions/html-block-repair.extension.d.ts +24 -0
- package/dist/extensions/html-block-repair.extension.d.ts.map +1 -0
- package/dist/extensions/html-block-repair.extension.js +47 -0
- package/dist/extensions/html-block-repair.extension.js.map +1 -0
- package/dist/extensions/svg-renderer.extension.d.ts +58 -0
- package/dist/extensions/svg-renderer.extension.d.ts.map +1 -0
- package/dist/extensions/svg-renderer.extension.js +83 -0
- package/dist/extensions/svg-renderer.extension.js.map +1 -0
- package/dist/helpers/escape.d.ts +9 -0
- package/dist/helpers/escape.d.ts.map +1 -0
- package/dist/helpers/escape.js +16 -0
- package/dist/helpers/escape.js.map +1 -0
- package/dist/helpers/language.d.ts +14 -0
- package/dist/helpers/language.d.ts.map +1 -0
- package/dist/helpers/language.js +68 -0
- package/dist/helpers/language.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/types/markdown.types.d.ts +213 -0
- package/dist/types/markdown.types.d.ts.map +1 -0
- package/dist/types/markdown.types.js +25 -0
- package/dist/types/markdown.types.js.map +1 -0
- package/package.json +37 -6
- package/README.md +0 -45
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { TokensList } from 'marked';
|
|
2
|
+
import { MarkdownConfig, ResolvedMarkdownConfig, HeadingInfo, HighlightFunction } from '../types/markdown.types';
|
|
3
|
+
/**
|
|
4
|
+
* Options that tune a single {@link MarkdownEngine.configureMarked} call.
|
|
5
|
+
*/
|
|
6
|
+
export interface ConfigureMarkedOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Syntax-highlight function used by the HTML output path. The engine itself
|
|
9
|
+
* carries no highlighter dependency — web callers pass a Prism-backed
|
|
10
|
+
* function; the token (AST) path ignores this entirely.
|
|
11
|
+
*/
|
|
12
|
+
highlightFn?: HighlightFunction;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Framework-agnostic markdown engine.
|
|
16
|
+
*
|
|
17
|
+
* Owns the `marked` configuration and the MemberJunction custom extensions
|
|
18
|
+
* (SVG code blocks, collapsible headings, GitHub alerts, heading ids,
|
|
19
|
+
* smartypants). It produces either:
|
|
20
|
+
*
|
|
21
|
+
* - an **HTML string** via {@link parseToHtml} — consumed by the Angular
|
|
22
|
+
* `ng-markdown` component (which then does its DOM post-processing), or
|
|
23
|
+
* - a **token tree (AST)** via {@link parseToTokens} — consumed by the React
|
|
24
|
+
* Native renderer (and any other non-DOM target).
|
|
25
|
+
*
|
|
26
|
+
* The engine has no DOM, Prism, or Mermaid dependency. Highlighting is injected
|
|
27
|
+
* by the caller; Mermaid and copy-button wiring are the host's responsibility.
|
|
28
|
+
*/
|
|
29
|
+
export declare class MarkdownEngine {
|
|
30
|
+
private marked;
|
|
31
|
+
private currentConfig;
|
|
32
|
+
private headingList;
|
|
33
|
+
private highlightFn?;
|
|
34
|
+
constructor();
|
|
35
|
+
/**
|
|
36
|
+
* (Re)build the internal marked instance from the given configuration.
|
|
37
|
+
*
|
|
38
|
+
* Each call constructs a **fresh** `Marked` instance and re-applies the full
|
|
39
|
+
* extension chain in a fixed order (html-block-repair → svg → highlight →
|
|
40
|
+
* heading ids → alerts → collapsible headings → smartypants). Ordering matters:
|
|
41
|
+
* the SVG renderer must run before the highlighter so `svg` fences are not
|
|
42
|
+
* highlighted as code.
|
|
43
|
+
*
|
|
44
|
+
* The injected highlight function is **sticky**: it is only replaced when
|
|
45
|
+
* `options.highlightFn` is explicitly provided. This lets `parseToHtml`/
|
|
46
|
+
* `parseToTokens` apply per-call config overrides (which re-invoke this method
|
|
47
|
+
* *without* options) without dropping the highlighter the host wired up once.
|
|
48
|
+
*
|
|
49
|
+
* @param config Markdown configuration; merged over {@link DEFAULT_MARKDOWN_CONFIG}.
|
|
50
|
+
* @param options Engine options, e.g. an injected {@link HighlightFunction}.
|
|
51
|
+
*/
|
|
52
|
+
configureMarked(config: MarkdownConfig, options?: ConfigureMarkedOptions): void;
|
|
53
|
+
/**
|
|
54
|
+
* Parse markdown to an HTML string.
|
|
55
|
+
*
|
|
56
|
+
* This is the web output path. Note it does NOT perform the DOM-based
|
|
57
|
+
* `unwrapMiscodedHtml` fixup that the Angular service applies afterward —
|
|
58
|
+
* that step requires a real DOM and stays in the host.
|
|
59
|
+
*
|
|
60
|
+
* @param markdown The markdown string to parse
|
|
61
|
+
* @param config Optional config overrides for this parse operation
|
|
62
|
+
* @returns The rendered HTML string
|
|
63
|
+
*/
|
|
64
|
+
parseToHtml(markdown: string, config?: Partial<MarkdownConfig>): string;
|
|
65
|
+
/**
|
|
66
|
+
* Parse markdown to a token tree (AST).
|
|
67
|
+
*
|
|
68
|
+
* This is the framework-agnostic path consumed by non-DOM renderers (React
|
|
69
|
+
* Native, etc.). Custom block tokenizers (e.g. `svgCodeBlock`) are applied,
|
|
70
|
+
* but render-time hooks (highlight markup, collapsible HTML wrapping) are
|
|
71
|
+
* NOT — the renderer decides how to present each token.
|
|
72
|
+
*
|
|
73
|
+
* @param markdown The markdown string to tokenize
|
|
74
|
+
* @param config Optional config overrides for this parse operation
|
|
75
|
+
* @returns The marked token list
|
|
76
|
+
*/
|
|
77
|
+
parseToTokens(markdown: string, config?: Partial<MarkdownConfig>): TokensList;
|
|
78
|
+
/**
|
|
79
|
+
* Get the list of headings from the last HTML parse.
|
|
80
|
+
* Useful for building a table of contents.
|
|
81
|
+
*/
|
|
82
|
+
getHeadingList(): HeadingInfo[];
|
|
83
|
+
/**
|
|
84
|
+
* Get the current resolved configuration.
|
|
85
|
+
*/
|
|
86
|
+
getConfig(): ResolvedMarkdownConfig;
|
|
87
|
+
/**
|
|
88
|
+
* Reset configuration to defaults (keeps any injected highlight function).
|
|
89
|
+
*/
|
|
90
|
+
resetConfig(): void;
|
|
91
|
+
/**
|
|
92
|
+
* Normalize indentation in HTML blocks to prevent marked from treating
|
|
93
|
+
* indented HTML as code blocks (4 spaces = code block in markdown).
|
|
94
|
+
*
|
|
95
|
+
* This finds HTML blocks (starting with common block-level tags) and
|
|
96
|
+
* removes ALL leading whitespace from lines within those blocks to ensure
|
|
97
|
+
* marked doesn't interpret any nested content as code blocks.
|
|
98
|
+
*/
|
|
99
|
+
private normalizeHtmlBlockIndentation;
|
|
100
|
+
/**
|
|
101
|
+
* Check if a line contains a self-closing tag or opens and closes the same tag
|
|
102
|
+
*/
|
|
103
|
+
private isSelfClosingLine;
|
|
104
|
+
/**
|
|
105
|
+
* Update the tag stack based on opening/closing tags in the line
|
|
106
|
+
*/
|
|
107
|
+
private updateTagStack;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=markdown-engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-engine.d.ts","sourceRoot":"","sources":["../../src/engine/markdown-engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,UAAU,EAAE,MAAM,QAAQ,CAAC;AAK5C,OAAO,EACL,cAAc,EAEd,sBAAsB,EACtB,WAAW,EACX,iBAAiB,EAClB,MAAM,yBAAyB,CAAC;AAMjC;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,WAAW,CAAC,EAAE,iBAAiB,CAAC;CACjC;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,aAAa,CAA0D;IAC/E,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,WAAW,CAAC,CAAoB;;IAOxC;;;;;;;;;;;;;;;;OAgBG;IACI,eAAe,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,IAAI;IAkFtF;;;;;;;;;;OAUG;IACI,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,MAAM;IA6B9E;;;;;;;;;;;OAWG;IACI,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,GAAG,UAAU;IAiBpF;;;OAGG;IACI,cAAc,IAAI,WAAW,EAAE;IAItC;;OAEG;IACI,SAAS,IAAI,sBAAsB;IAI1C;;OAEG;IACI,WAAW,IAAI,IAAI;IAI1B;;;;;;;OAOG;IACH,OAAO,CAAC,6BAA6B;IAsDrC;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAWzB;;OAEG;IACH,OAAO,CAAC,cAAc;CA+BvB"}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { Marked } from 'marked';
|
|
2
|
+
import { markedHighlight } from 'marked-highlight';
|
|
3
|
+
import { gfmHeadingId, getHeadingList } from 'marked-gfm-heading-id';
|
|
4
|
+
import markedAlert from 'marked-alert';
|
|
5
|
+
import { markedSmartypants } from 'marked-smartypants';
|
|
6
|
+
import { DEFAULT_MARKDOWN_CONFIG } from '../types/markdown.types';
|
|
7
|
+
import { createCollapsibleHeadingsExtension } from '../extensions/collapsible-headings.extension';
|
|
8
|
+
import { createSvgRendererExtension } from '../extensions/svg-renderer.extension';
|
|
9
|
+
import { createHtmlBlockRepairExtension } from '../extensions/html-block-repair.extension';
|
|
10
|
+
import { escapeHtml } from '../helpers/escape';
|
|
11
|
+
/**
|
|
12
|
+
* Framework-agnostic markdown engine.
|
|
13
|
+
*
|
|
14
|
+
* Owns the `marked` configuration and the MemberJunction custom extensions
|
|
15
|
+
* (SVG code blocks, collapsible headings, GitHub alerts, heading ids,
|
|
16
|
+
* smartypants). It produces either:
|
|
17
|
+
*
|
|
18
|
+
* - an **HTML string** via {@link parseToHtml} — consumed by the Angular
|
|
19
|
+
* `ng-markdown` component (which then does its DOM post-processing), or
|
|
20
|
+
* - a **token tree (AST)** via {@link parseToTokens} — consumed by the React
|
|
21
|
+
* Native renderer (and any other non-DOM target).
|
|
22
|
+
*
|
|
23
|
+
* The engine has no DOM, Prism, or Mermaid dependency. Highlighting is injected
|
|
24
|
+
* by the caller; Mermaid and copy-button wiring are the host's responsibility.
|
|
25
|
+
*/
|
|
26
|
+
export class MarkdownEngine {
|
|
27
|
+
constructor() {
|
|
28
|
+
this.currentConfig = { ...DEFAULT_MARKDOWN_CONFIG };
|
|
29
|
+
this.headingList = [];
|
|
30
|
+
this.marked = new Marked();
|
|
31
|
+
this.configureMarked(this.currentConfig);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* (Re)build the internal marked instance from the given configuration.
|
|
35
|
+
*
|
|
36
|
+
* Each call constructs a **fresh** `Marked` instance and re-applies the full
|
|
37
|
+
* extension chain in a fixed order (html-block-repair → svg → highlight →
|
|
38
|
+
* heading ids → alerts → collapsible headings → smartypants). Ordering matters:
|
|
39
|
+
* the SVG renderer must run before the highlighter so `svg` fences are not
|
|
40
|
+
* highlighted as code.
|
|
41
|
+
*
|
|
42
|
+
* The injected highlight function is **sticky**: it is only replaced when
|
|
43
|
+
* `options.highlightFn` is explicitly provided. This lets `parseToHtml`/
|
|
44
|
+
* `parseToTokens` apply per-call config overrides (which re-invoke this method
|
|
45
|
+
* *without* options) without dropping the highlighter the host wired up once.
|
|
46
|
+
*
|
|
47
|
+
* @param config Markdown configuration; merged over {@link DEFAULT_MARKDOWN_CONFIG}.
|
|
48
|
+
* @param options Engine options, e.g. an injected {@link HighlightFunction}.
|
|
49
|
+
*/
|
|
50
|
+
configureMarked(config, options) {
|
|
51
|
+
this.currentConfig = { ...DEFAULT_MARKDOWN_CONFIG, ...config };
|
|
52
|
+
if (options && 'highlightFn' in options) {
|
|
53
|
+
this.highlightFn = options.highlightFn;
|
|
54
|
+
}
|
|
55
|
+
// Create a fresh Marked instance
|
|
56
|
+
this.marked = new Marked();
|
|
57
|
+
// Configure base options
|
|
58
|
+
this.marked.setOptions({
|
|
59
|
+
gfm: true,
|
|
60
|
+
breaks: true
|
|
61
|
+
});
|
|
62
|
+
const extensions = [];
|
|
63
|
+
// Repair HTML blocks split by a blank line (e.g. PRD mockups) so embedded
|
|
64
|
+
// raw HTML renders instead of showing as an escaped code block. Always on -
|
|
65
|
+
// precisely scoped to misparsed HTML, leaves prose and fenced code untouched.
|
|
66
|
+
extensions.push(createHtmlBlockRepairExtension());
|
|
67
|
+
// SVG code block renderer - MUST be before syntax highlighting
|
|
68
|
+
// so it can intercept svg blocks before the highlighter processes them.
|
|
69
|
+
if (this.currentConfig.enableSvgRenderer) {
|
|
70
|
+
extensions.push(createSvgRendererExtension());
|
|
71
|
+
}
|
|
72
|
+
// Syntax highlighting via an injected highlight function (e.g. Prism on web).
|
|
73
|
+
if (this.currentConfig.enableHighlight && this.highlightFn) {
|
|
74
|
+
const highlightFn = this.highlightFn;
|
|
75
|
+
const svgEnabled = this.currentConfig.enableSvgRenderer;
|
|
76
|
+
extensions.push(markedHighlight({
|
|
77
|
+
langPrefix: 'language-',
|
|
78
|
+
highlight: (code, lang) => {
|
|
79
|
+
// Skip SVG blocks - they're handled by the SVG renderer
|
|
80
|
+
if (lang === 'svg' && svgEnabled) {
|
|
81
|
+
return code;
|
|
82
|
+
}
|
|
83
|
+
return highlightFn(code, lang);
|
|
84
|
+
}
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
// GitHub-style heading IDs
|
|
88
|
+
if (this.currentConfig.enableHeadingIds) {
|
|
89
|
+
extensions.push(gfmHeadingId({
|
|
90
|
+
prefix: this.currentConfig.headingIdPrefix
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
// GitHub-style alerts
|
|
94
|
+
if (this.currentConfig.enableAlerts) {
|
|
95
|
+
extensions.push(markedAlert());
|
|
96
|
+
}
|
|
97
|
+
// Collapsible headings (custom extension, HTML output path)
|
|
98
|
+
if (this.currentConfig.enableCollapsibleHeadings) {
|
|
99
|
+
extensions.push(createCollapsibleHeadingsExtension({
|
|
100
|
+
startLevel: this.currentConfig.collapsibleHeadingLevel,
|
|
101
|
+
defaultExpanded: this.currentConfig.collapsibleDefaultExpanded,
|
|
102
|
+
autoExpandLevels: this.currentConfig.autoExpandLevels
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
// Smartypants for typography (curly quotes, em/en dashes, ellipses)
|
|
106
|
+
if (this.currentConfig.enableSmartypants) {
|
|
107
|
+
extensions.push(markedSmartypants());
|
|
108
|
+
}
|
|
109
|
+
// Apply all extensions
|
|
110
|
+
if (extensions.length > 0) {
|
|
111
|
+
this.marked.use(...extensions);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Parse markdown to an HTML string.
|
|
116
|
+
*
|
|
117
|
+
* This is the web output path. Note it does NOT perform the DOM-based
|
|
118
|
+
* `unwrapMiscodedHtml` fixup that the Angular service applies afterward —
|
|
119
|
+
* that step requires a real DOM and stays in the host.
|
|
120
|
+
*
|
|
121
|
+
* @param markdown The markdown string to parse
|
|
122
|
+
* @param config Optional config overrides for this parse operation
|
|
123
|
+
* @returns The rendered HTML string
|
|
124
|
+
*/
|
|
125
|
+
parseToHtml(markdown, config) {
|
|
126
|
+
if (!markdown)
|
|
127
|
+
return '';
|
|
128
|
+
if (config) {
|
|
129
|
+
this.configureMarked({ ...this.currentConfig, ...config });
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
// Preprocess markdown to fix indentation in HTML blocks so marked does
|
|
133
|
+
// not treat indented HTML as code blocks (pure string transform).
|
|
134
|
+
let processedMarkdown = markdown;
|
|
135
|
+
if (this.currentConfig.enableHtml) {
|
|
136
|
+
processedMarkdown = this.normalizeHtmlBlockIndentation(markdown);
|
|
137
|
+
}
|
|
138
|
+
const html = this.marked.parse(processedMarkdown);
|
|
139
|
+
// Capture heading list after parsing
|
|
140
|
+
if (this.currentConfig.enableHeadingIds) {
|
|
141
|
+
this.headingList = getHeadingList();
|
|
142
|
+
}
|
|
143
|
+
return html;
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
console.error('Markdown parsing error:', error);
|
|
147
|
+
return `<pre class="markdown-error">${escapeHtml(markdown)}</pre>`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Parse markdown to a token tree (AST).
|
|
152
|
+
*
|
|
153
|
+
* This is the framework-agnostic path consumed by non-DOM renderers (React
|
|
154
|
+
* Native, etc.). Custom block tokenizers (e.g. `svgCodeBlock`) are applied,
|
|
155
|
+
* but render-time hooks (highlight markup, collapsible HTML wrapping) are
|
|
156
|
+
* NOT — the renderer decides how to present each token.
|
|
157
|
+
*
|
|
158
|
+
* @param markdown The markdown string to tokenize
|
|
159
|
+
* @param config Optional config overrides for this parse operation
|
|
160
|
+
* @returns The marked token list
|
|
161
|
+
*/
|
|
162
|
+
parseToTokens(markdown, config) {
|
|
163
|
+
if (config) {
|
|
164
|
+
this.configureMarked({ ...this.currentConfig, ...config });
|
|
165
|
+
}
|
|
166
|
+
if (!markdown) {
|
|
167
|
+
return this.marked.lexer('');
|
|
168
|
+
}
|
|
169
|
+
let processedMarkdown = markdown;
|
|
170
|
+
if (this.currentConfig.enableHtml) {
|
|
171
|
+
processedMarkdown = this.normalizeHtmlBlockIndentation(markdown);
|
|
172
|
+
}
|
|
173
|
+
return this.marked.lexer(processedMarkdown);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Get the list of headings from the last HTML parse.
|
|
177
|
+
* Useful for building a table of contents.
|
|
178
|
+
*/
|
|
179
|
+
getHeadingList() {
|
|
180
|
+
return this.headingList;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Get the current resolved configuration.
|
|
184
|
+
*/
|
|
185
|
+
getConfig() {
|
|
186
|
+
return { ...this.currentConfig };
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Reset configuration to defaults (keeps any injected highlight function).
|
|
190
|
+
*/
|
|
191
|
+
resetConfig() {
|
|
192
|
+
this.configureMarked(DEFAULT_MARKDOWN_CONFIG);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Normalize indentation in HTML blocks to prevent marked from treating
|
|
196
|
+
* indented HTML as code blocks (4 spaces = code block in markdown).
|
|
197
|
+
*
|
|
198
|
+
* This finds HTML blocks (starting with common block-level tags) and
|
|
199
|
+
* removes ALL leading whitespace from lines within those blocks to ensure
|
|
200
|
+
* marked doesn't interpret any nested content as code blocks.
|
|
201
|
+
*/
|
|
202
|
+
normalizeHtmlBlockIndentation(markdown) {
|
|
203
|
+
// Match HTML blocks that start with common block-level tags
|
|
204
|
+
const htmlBlockTags = [
|
|
205
|
+
'div', 'table', 'thead', 'tbody', 'tr', 'td', 'th',
|
|
206
|
+
'ul', 'ol', 'li', 'p', 'section', 'article', 'header',
|
|
207
|
+
'footer', 'nav', 'main', 'aside', 'form', 'svg', 'figure'
|
|
208
|
+
];
|
|
209
|
+
const tagPattern = htmlBlockTags.join('|');
|
|
210
|
+
// Match opening tag at start of line (possibly with leading whitespace)
|
|
211
|
+
const htmlBlockStartRegex = new RegExp(`^[ \\t]*<(${tagPattern})\\b`, 'i');
|
|
212
|
+
const lines = markdown.split('\n');
|
|
213
|
+
const result = [];
|
|
214
|
+
let inHtmlBlock = false;
|
|
215
|
+
const tagStack = [];
|
|
216
|
+
for (const line of lines) {
|
|
217
|
+
const trimmedLine = line.trimStart();
|
|
218
|
+
if (!inHtmlBlock) {
|
|
219
|
+
// Check if this line starts an HTML block
|
|
220
|
+
const match = trimmedLine.match(htmlBlockStartRegex);
|
|
221
|
+
if (match) {
|
|
222
|
+
inHtmlBlock = true;
|
|
223
|
+
const tag = match[1].toLowerCase();
|
|
224
|
+
// Push to stack if it's not a self-closing tag on this line
|
|
225
|
+
if (!this.isSelfClosingLine(trimmedLine, tag)) {
|
|
226
|
+
tagStack.push(tag);
|
|
227
|
+
}
|
|
228
|
+
// Remove leading indentation
|
|
229
|
+
result.push(trimmedLine);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
result.push(line);
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
// We're inside an HTML block - remove ALL leading whitespace
|
|
236
|
+
// to prevent any nested content from being treated as code blocks.
|
|
237
|
+
this.updateTagStack(trimmedLine, tagStack, htmlBlockTags);
|
|
238
|
+
result.push(trimmedLine);
|
|
239
|
+
// Check if we've closed all HTML blocks
|
|
240
|
+
if (tagStack.length === 0) {
|
|
241
|
+
inHtmlBlock = false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return result.join('\n');
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Check if a line contains a self-closing tag or opens and closes the same tag
|
|
249
|
+
*/
|
|
250
|
+
isSelfClosingLine(line, tag) {
|
|
251
|
+
// Check for self-closing syntax: <tag ... />
|
|
252
|
+
if (new RegExp(`<${tag}[^>]*/>`, 'i').test(line)) {
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
// Check if tag opens and closes on same line: <tag>...</tag>
|
|
256
|
+
const openCount = (line.match(new RegExp(`<${tag}\\b`, 'gi')) || []).length;
|
|
257
|
+
const closeCount = (line.match(new RegExp(`</${tag}>`, 'gi')) || []).length;
|
|
258
|
+
return openCount > 0 && openCount === closeCount;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Update the tag stack based on opening/closing tags in the line
|
|
262
|
+
*/
|
|
263
|
+
updateTagStack(line, tagStack, validTags) {
|
|
264
|
+
const openTagRegex = /<(\w+)\b[^>]*(?<!\/)>/gi;
|
|
265
|
+
const closeTagRegex = /<\/(\w+)>/gi;
|
|
266
|
+
let match;
|
|
267
|
+
// Process closing tags first (they might close tags opened earlier)
|
|
268
|
+
while ((match = closeTagRegex.exec(line)) !== null) {
|
|
269
|
+
const tag = match[1].toLowerCase();
|
|
270
|
+
const idx = tagStack.lastIndexOf(tag);
|
|
271
|
+
if (idx !== -1) {
|
|
272
|
+
tagStack.splice(idx, 1);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Process opening tags
|
|
276
|
+
while ((match = openTagRegex.exec(line)) !== null) {
|
|
277
|
+
const tag = match[1].toLowerCase();
|
|
278
|
+
// Only track block-level tags we care about
|
|
279
|
+
if (validTags.includes(tag)) {
|
|
280
|
+
if (!this.isSelfClosingLine(line, tag)) {
|
|
281
|
+
const closeRegex = new RegExp(`</${tag}>`, 'gi');
|
|
282
|
+
const opens = (line.match(new RegExp(`<${tag}\\b`, 'gi')) || []).length;
|
|
283
|
+
const closes = (line.match(closeRegex) || []).length;
|
|
284
|
+
if (opens > closes) {
|
|
285
|
+
tagStack.push(tag);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
//# sourceMappingURL=markdown-engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown-engine.js","sourceRoot":"","sources":["../../src/engine/markdown-engine.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { MarkedExtension } from 'marked';
|
|
2
|
+
/**
|
|
3
|
+
* Options for the collapsible headings extension
|
|
4
|
+
*/
|
|
5
|
+
export interface CollapsibleHeadingsOptions {
|
|
6
|
+
/**
|
|
7
|
+
* The minimum heading level to make collapsible (1-6)
|
|
8
|
+
* Headings at this level and below will be collapsible
|
|
9
|
+
* @default 2
|
|
10
|
+
*/
|
|
11
|
+
startLevel?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
12
|
+
/**
|
|
13
|
+
* Whether sections should be expanded by default
|
|
14
|
+
* @default true
|
|
15
|
+
*/
|
|
16
|
+
defaultExpanded?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Specify which heading levels should start expanded.
|
|
19
|
+
* Takes precedence over defaultExpanded for specified levels.
|
|
20
|
+
* @default undefined (uses defaultExpanded for all levels)
|
|
21
|
+
*/
|
|
22
|
+
autoExpandLevels?: number[];
|
|
23
|
+
/**
|
|
24
|
+
* CSS class prefix for generated elements
|
|
25
|
+
* @default 'collapsible'
|
|
26
|
+
*/
|
|
27
|
+
classPrefix?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Creates a marked extension that wraps heading sections in collapsible containers.
|
|
31
|
+
*
|
|
32
|
+
* This extension transforms headings into clickable toggles that can expand/collapse
|
|
33
|
+
* the content that follows them (until the next heading of equal or higher level).
|
|
34
|
+
*
|
|
35
|
+
* The key innovation is that child sections are properly NESTED inside parent sections,
|
|
36
|
+
* so collapsing a parent will hide all its children.
|
|
37
|
+
*
|
|
38
|
+
* NOTE: This extension targets the HTML output path. It produces the nested
|
|
39
|
+
* `<div class="collapsible-section">` structure that the web renderer styles and
|
|
40
|
+
* wires up. React Native does not use this extension — it derives collapsible
|
|
41
|
+
* structure from the heading tokens directly.
|
|
42
|
+
*
|
|
43
|
+
* The generated HTML structure (properly nested):
|
|
44
|
+
* ```html
|
|
45
|
+
* <div class="collapsible-section" data-level="2">
|
|
46
|
+
* <div class="collapsible-heading-wrapper">
|
|
47
|
+
* <h2 class="collapsible-heading" id="...">Parent Heading</h2>
|
|
48
|
+
* </div>
|
|
49
|
+
* <div class="collapsible-content">
|
|
50
|
+
* <p>Content under parent...</p>
|
|
51
|
+
* <div class="collapsible-section" data-level="3">
|
|
52
|
+
* ...
|
|
53
|
+
* </div>
|
|
54
|
+
* </div>
|
|
55
|
+
* </div>
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* Note: The toggle button is added dynamically by the host (e.g. the Angular
|
|
59
|
+
* component) after rendering to avoid issues with HTML sanitizers.
|
|
60
|
+
*/
|
|
61
|
+
export declare function createCollapsibleHeadingsExtension(options?: CollapsibleHeadingsOptions): MarkedExtension;
|
|
62
|
+
//# sourceMappingURL=collapsible-headings.extension.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collapsible-headings.extension.d.ts","sourceRoot":"","sources":["../../src/extensions/collapsible-headings.extension.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAU,MAAM,QAAQ,CAAC;AAEjD;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;;OAIG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAEnC;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE5B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAgBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,kCAAkC,CAChD,OAAO,CAAC,EAAE,0BAA0B,GACnC,eAAe,CA8BjB"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
const DEFAULT_OPTIONS = {
|
|
2
|
+
startLevel: 2,
|
|
3
|
+
defaultExpanded: true,
|
|
4
|
+
autoExpandLevels: undefined,
|
|
5
|
+
classPrefix: 'collapsible'
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Creates a marked extension that wraps heading sections in collapsible containers.
|
|
9
|
+
*
|
|
10
|
+
* This extension transforms headings into clickable toggles that can expand/collapse
|
|
11
|
+
* the content that follows them (until the next heading of equal or higher level).
|
|
12
|
+
*
|
|
13
|
+
* The key innovation is that child sections are properly NESTED inside parent sections,
|
|
14
|
+
* so collapsing a parent will hide all its children.
|
|
15
|
+
*
|
|
16
|
+
* NOTE: This extension targets the HTML output path. It produces the nested
|
|
17
|
+
* `<div class="collapsible-section">` structure that the web renderer styles and
|
|
18
|
+
* wires up. React Native does not use this extension — it derives collapsible
|
|
19
|
+
* structure from the heading tokens directly.
|
|
20
|
+
*
|
|
21
|
+
* The generated HTML structure (properly nested):
|
|
22
|
+
* ```html
|
|
23
|
+
* <div class="collapsible-section" data-level="2">
|
|
24
|
+
* <div class="collapsible-heading-wrapper">
|
|
25
|
+
* <h2 class="collapsible-heading" id="...">Parent Heading</h2>
|
|
26
|
+
* </div>
|
|
27
|
+
* <div class="collapsible-content">
|
|
28
|
+
* <p>Content under parent...</p>
|
|
29
|
+
* <div class="collapsible-section" data-level="3">
|
|
30
|
+
* ...
|
|
31
|
+
* </div>
|
|
32
|
+
* </div>
|
|
33
|
+
* </div>
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* Note: The toggle button is added dynamically by the host (e.g. the Angular
|
|
37
|
+
* component) after rendering to avoid issues with HTML sanitizers.
|
|
38
|
+
*/
|
|
39
|
+
export function createCollapsibleHeadingsExtension(options) {
|
|
40
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
41
|
+
return {
|
|
42
|
+
renderer: {
|
|
43
|
+
// Mark headings with a special marker that we'll process in postprocess
|
|
44
|
+
heading(token) {
|
|
45
|
+
const { depth, text } = token;
|
|
46
|
+
const escapedText = text.toLowerCase().replace(/[^\w]+/g, '-');
|
|
47
|
+
const id = `heading-${escapedText}`;
|
|
48
|
+
// Only make headings collapsible at or below the start level
|
|
49
|
+
if (depth >= opts.startLevel) {
|
|
50
|
+
// Use a marker format that we'll parse in postprocess
|
|
51
|
+
// Format: <!--COLLAPSIBLE_HEADING:level:id:text-->
|
|
52
|
+
const marker = `<!--COLLAPSIBLE_HEADING:${depth}:${id}:${encodeURIComponent(text)}-->`;
|
|
53
|
+
return `${marker}<h${depth} id="${id}">${text}</h${depth}>\n`;
|
|
54
|
+
}
|
|
55
|
+
// Regular heading rendering for levels above startLevel
|
|
56
|
+
return `<h${depth} id="${id}">${text}</h${depth}>\n`;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
hooks: {
|
|
60
|
+
postprocess(html) {
|
|
61
|
+
return restructureToNestedSections(html, opts);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Restructures flat HTML with heading markers into properly nested collapsible sections.
|
|
68
|
+
* This is the key function that creates the hierarchical structure needed for
|
|
69
|
+
* parent collapse to affect children.
|
|
70
|
+
*/
|
|
71
|
+
function restructureToNestedSections(html, opts) {
|
|
72
|
+
// Parse the HTML to find all content chunks and headings
|
|
73
|
+
const markerRegex = /<!--COLLAPSIBLE_HEADING:(\d):([^:]+):([^>]+)-->/g;
|
|
74
|
+
// Split HTML by markers while capturing the markers
|
|
75
|
+
const parts = [];
|
|
76
|
+
let lastIndex = 0;
|
|
77
|
+
let match;
|
|
78
|
+
while ((match = markerRegex.exec(html)) !== null) {
|
|
79
|
+
// Add content before this marker
|
|
80
|
+
if (match.index > lastIndex) {
|
|
81
|
+
const content = html.slice(lastIndex, match.index);
|
|
82
|
+
if (content.trim()) {
|
|
83
|
+
parts.push({ type: 'content', content });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Add the heading marker info
|
|
87
|
+
parts.push({
|
|
88
|
+
type: 'heading',
|
|
89
|
+
content: '', // Will be filled with the actual h tag
|
|
90
|
+
level: parseInt(match[1], 10),
|
|
91
|
+
id: match[2],
|
|
92
|
+
text: decodeURIComponent(match[3])
|
|
93
|
+
});
|
|
94
|
+
lastIndex = match.index + match[0].length;
|
|
95
|
+
}
|
|
96
|
+
// Add remaining content after last marker
|
|
97
|
+
if (lastIndex < html.length) {
|
|
98
|
+
const content = html.slice(lastIndex);
|
|
99
|
+
if (content.trim()) {
|
|
100
|
+
parts.push({ type: 'content', content });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// If no collapsible headings, return original HTML
|
|
104
|
+
if (!parts.some(p => p.type === 'heading')) {
|
|
105
|
+
return html;
|
|
106
|
+
}
|
|
107
|
+
// Now build the nested structure
|
|
108
|
+
return buildNestedStructure(parts, opts);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Determine if a heading level should start expanded based on options
|
|
112
|
+
*/
|
|
113
|
+
function shouldLevelBeExpanded(level, opts) {
|
|
114
|
+
// If autoExpandLevels is specified, use it
|
|
115
|
+
if (opts.autoExpandLevels !== undefined) {
|
|
116
|
+
return opts.autoExpandLevels.includes(level);
|
|
117
|
+
}
|
|
118
|
+
// Otherwise fall back to defaultExpanded
|
|
119
|
+
return opts.defaultExpanded;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Builds the nested HTML structure from the parsed parts.
|
|
123
|
+
*/
|
|
124
|
+
function buildNestedStructure(parts, opts) {
|
|
125
|
+
const result = [];
|
|
126
|
+
// Stack tracks open sections: { level }
|
|
127
|
+
const sectionStack = [];
|
|
128
|
+
for (let i = 0; i < parts.length; i++) {
|
|
129
|
+
const part = parts[i];
|
|
130
|
+
if (part.type === 'content') {
|
|
131
|
+
// Regular content - just add it
|
|
132
|
+
result.push(part.content);
|
|
133
|
+
}
|
|
134
|
+
else if (part.type === 'heading' && part.level !== undefined) {
|
|
135
|
+
const level = part.level;
|
|
136
|
+
// Close any sections at same or higher level (lower number = higher level)
|
|
137
|
+
while (sectionStack.length > 0 && sectionStack[sectionStack.length - 1].level >= level) {
|
|
138
|
+
// Close the content div and section div
|
|
139
|
+
result.push('</div></div>');
|
|
140
|
+
sectionStack.pop();
|
|
141
|
+
}
|
|
142
|
+
// Determine expanded state for this specific level
|
|
143
|
+
const isExpanded = shouldLevelBeExpanded(level, opts);
|
|
144
|
+
const expandedClass = isExpanded ? '' : ' collapsed';
|
|
145
|
+
// Extract the actual h tag from the next content piece if it starts with it
|
|
146
|
+
let headingHtml = `<h${level} class="${opts.classPrefix}-heading" id="${part.id}">${part.text}</h${level}>`;
|
|
147
|
+
// Look ahead to find and remove the actual h tag from content
|
|
148
|
+
if (i + 1 < parts.length && parts[i + 1].type === 'content') {
|
|
149
|
+
const nextContent = parts[i + 1].content;
|
|
150
|
+
const hTagRegex = new RegExp(`<h${level}[^>]*id="${part.id}"[^>]*>[^<]*</h${level}>\\s*`);
|
|
151
|
+
const hTagMatch = nextContent.match(hTagRegex);
|
|
152
|
+
if (hTagMatch) {
|
|
153
|
+
// Use the actual h tag and remove it from content
|
|
154
|
+
headingHtml = hTagMatch[0].replace(`<h${level}`, `<h${level} class="${opts.classPrefix}-heading"`);
|
|
155
|
+
parts[i + 1].content = nextContent.replace(hTagRegex, '');
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Open new section
|
|
159
|
+
result.push(`<div class="${opts.classPrefix}-section${expandedClass}" data-level="${level}">`);
|
|
160
|
+
result.push(`<div class="${opts.classPrefix}-heading-wrapper">`);
|
|
161
|
+
result.push(headingHtml);
|
|
162
|
+
result.push('</div>');
|
|
163
|
+
result.push(`<div class="${opts.classPrefix}-content">`);
|
|
164
|
+
sectionStack.push({ level });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// Close any remaining open sections
|
|
168
|
+
while (sectionStack.length > 0) {
|
|
169
|
+
result.push('</div></div>');
|
|
170
|
+
sectionStack.pop();
|
|
171
|
+
}
|
|
172
|
+
return result.join('');
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=collapsible-headings.extension.js.map
|