@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.
Files changed (34) hide show
  1. package/dist/engine/markdown-engine.d.ts +109 -0
  2. package/dist/engine/markdown-engine.d.ts.map +1 -0
  3. package/dist/engine/markdown-engine.js +292 -0
  4. package/dist/engine/markdown-engine.js.map +1 -0
  5. package/dist/extensions/collapsible-headings.extension.d.ts +62 -0
  6. package/dist/extensions/collapsible-headings.extension.d.ts.map +1 -0
  7. package/dist/extensions/collapsible-headings.extension.js +174 -0
  8. package/dist/extensions/collapsible-headings.extension.js.map +1 -0
  9. package/dist/extensions/html-block-repair.extension.d.ts +24 -0
  10. package/dist/extensions/html-block-repair.extension.d.ts.map +1 -0
  11. package/dist/extensions/html-block-repair.extension.js +47 -0
  12. package/dist/extensions/html-block-repair.extension.js.map +1 -0
  13. package/dist/extensions/svg-renderer.extension.d.ts +58 -0
  14. package/dist/extensions/svg-renderer.extension.d.ts.map +1 -0
  15. package/dist/extensions/svg-renderer.extension.js +83 -0
  16. package/dist/extensions/svg-renderer.extension.js.map +1 -0
  17. package/dist/helpers/escape.d.ts +9 -0
  18. package/dist/helpers/escape.d.ts.map +1 -0
  19. package/dist/helpers/escape.js +16 -0
  20. package/dist/helpers/escape.js.map +1 -0
  21. package/dist/helpers/language.d.ts +14 -0
  22. package/dist/helpers/language.d.ts.map +1 -0
  23. package/dist/helpers/language.js +68 -0
  24. package/dist/helpers/language.js.map +1 -0
  25. package/dist/index.d.ts +8 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +17 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/types/markdown.types.d.ts +213 -0
  30. package/dist/types/markdown.types.d.ts.map +1 -0
  31. package/dist/types/markdown.types.js +25 -0
  32. package/dist/types/markdown.types.js.map +1 -0
  33. package/package.json +37 -6
  34. 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