@memberjunction/ng-markdown 5.44.0 → 5.45.1

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 (46) hide show
  1. package/dist/__tests__/markdown.service.dom.test.d.ts +2 -0
  2. package/dist/__tests__/markdown.service.dom.test.d.ts.map +1 -0
  3. package/dist/__tests__/markdown.service.dom.test.js +86 -0
  4. package/dist/__tests__/markdown.service.dom.test.js.map +1 -0
  5. package/dist/__tests__/web-dom-utils.test.d.ts +2 -0
  6. package/dist/__tests__/web-dom-utils.test.d.ts.map +1 -0
  7. package/dist/__tests__/web-dom-utils.test.js +160 -0
  8. package/dist/__tests__/web-dom-utils.test.js.map +1 -0
  9. package/dist/lib/components/markdown.component.d.ts +1 -1
  10. package/dist/lib/components/markdown.component.d.ts.map +1 -1
  11. package/dist/lib/components/markdown.component.dom.test.js.map +1 -1
  12. package/dist/lib/components/markdown.component.js +1 -1
  13. package/dist/lib/components/markdown.component.js.map +1 -1
  14. package/dist/lib/extensions/code-copy.extension.d.ts +0 -7
  15. package/dist/lib/extensions/code-copy.extension.d.ts.map +1 -1
  16. package/dist/lib/extensions/code-copy.extension.js +1 -58
  17. package/dist/lib/extensions/code-copy.extension.js.map +1 -1
  18. package/dist/lib/extensions/collapsible-headings.extension.d.ts +5 -67
  19. package/dist/lib/extensions/collapsible-headings.extension.d.ts.map +1 -1
  20. package/dist/lib/extensions/collapsible-headings.extension.js +5 -179
  21. package/dist/lib/extensions/collapsible-headings.extension.js.map +1 -1
  22. package/dist/lib/extensions/svg-renderer.extension.d.ts +5 -28
  23. package/dist/lib/extensions/svg-renderer.extension.d.ts.map +1 -1
  24. package/dist/lib/extensions/svg-renderer.extension.js +5 -72
  25. package/dist/lib/extensions/svg-renderer.extension.js.map +1 -1
  26. package/dist/lib/services/markdown.service.d.ts +27 -43
  27. package/dist/lib/services/markdown.service.d.ts.map +1 -1
  28. package/dist/lib/services/markdown.service.js +56 -235
  29. package/dist/lib/services/markdown.service.js.map +1 -1
  30. package/dist/public-api.d.ts +0 -1
  31. package/dist/public-api.d.ts.map +1 -1
  32. package/dist/public-api.js +6 -3
  33. package/dist/public-api.js.map +1 -1
  34. package/package.json +4 -7
  35. package/dist/__tests__/markdown-types.test.d.ts +0 -2
  36. package/dist/__tests__/markdown-types.test.d.ts.map +0 -1
  37. package/dist/__tests__/markdown-types.test.js +0 -78
  38. package/dist/__tests__/markdown-types.test.js.map +0 -1
  39. package/dist/lib/extensions/html-block-repair.extension.d.ts +0 -20
  40. package/dist/lib/extensions/html-block-repair.extension.d.ts.map +0 -1
  41. package/dist/lib/extensions/html-block-repair.extension.js +0 -43
  42. package/dist/lib/extensions/html-block-repair.extension.js.map +0 -1
  43. package/dist/lib/types/markdown.types.d.ts +0 -194
  44. package/dist/lib/types/markdown.types.d.ts.map +0 -1
  45. package/dist/lib/types/markdown.types.js +0 -25
  46. package/dist/lib/types/markdown.types.js.map +0 -1
@@ -1,10 +1,3 @@
1
- /**
2
- * Code copy extension utilities
3
- *
4
- * This module provides utilities for adding copy-to-clipboard functionality
5
- * to code blocks. Unlike other marked extensions, this operates on the DOM
6
- * after rendering since it requires adding interactive elements.
7
- */
8
1
  /**
9
2
  * Options for the code copy functionality
10
3
  */
@@ -1 +1 @@
1
- {"version":3,"file":"code-copy.extension.d.ts","sourceRoot":"","sources":["../../../src/lib/extensions/code-copy.extension.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;OAEG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAEhC;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAEjC;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAeD;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,WAAW,EACtB,OAAO,CAAC,EAAE,eAAe,GACxB,IAAI,CA8EN;AA6ED;;;GAGG;AACH,wBAAgB,+BAA+B,CAC7C,SAAS,EAAE,WAAW,EACtB,YAAY,GAAE,MAAuB,GACpC,IAAI,CAGN"}
1
+ {"version":3,"file":"code-copy.extension.d.ts","sourceRoot":"","sources":["../../../src/lib/extensions/code-copy.extension.ts"],"names":[],"mappings":"AASA;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;OAEG;IACH,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAEhC;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAEjC;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAeD;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,WAAW,EACtB,OAAO,CAAC,EAAE,eAAe,GACxB,IAAI,CA8EN;AAiBD;;;GAGG;AACH,wBAAgB,+BAA+B,CAC7C,SAAS,EAAE,WAAW,EACtB,YAAY,GAAE,MAAuB,GACpC,IAAI,CAGN"}
@@ -5,6 +5,7 @@
5
5
  * to code blocks. Unlike other marked extensions, this operates on the DOM
6
6
  * after rendering since it requires adding interactive elements.
7
7
  */
8
+ import { formatLanguageName } from '@memberjunction/markdown-core';
8
9
  const DEFAULT_OPTIONS = {
9
10
  buttonClass: 'code-copy-btn',
10
11
  toolbarClass: 'code-toolbar',
@@ -100,64 +101,6 @@ function extractLanguageFromCode(codeElement) {
100
101
  }
101
102
  return null;
102
103
  }
103
- /**
104
- * Format a language identifier for display
105
- */
106
- function formatLanguageName(language) {
107
- const languageMap = {
108
- js: 'JavaScript',
109
- ts: 'TypeScript',
110
- javascript: 'JavaScript',
111
- typescript: 'TypeScript',
112
- py: 'Python',
113
- python: 'Python',
114
- rb: 'Ruby',
115
- ruby: 'Ruby',
116
- cs: 'C#',
117
- csharp: 'C#',
118
- cpp: 'C++',
119
- 'c++': 'C++',
120
- html: 'HTML',
121
- css: 'CSS',
122
- scss: 'SCSS',
123
- sass: 'Sass',
124
- less: 'Less',
125
- json: 'JSON',
126
- yaml: 'YAML',
127
- yml: 'YAML',
128
- xml: 'XML',
129
- sql: 'SQL',
130
- bash: 'Bash',
131
- shell: 'Shell',
132
- sh: 'Shell',
133
- zsh: 'Zsh',
134
- powershell: 'PowerShell',
135
- ps1: 'PowerShell',
136
- md: 'Markdown',
137
- markdown: 'Markdown',
138
- graphql: 'GraphQL',
139
- gql: 'GraphQL',
140
- java: 'Java',
141
- kotlin: 'Kotlin',
142
- swift: 'Swift',
143
- go: 'Go',
144
- rust: 'Rust',
145
- php: 'PHP',
146
- r: 'R',
147
- dockerfile: 'Dockerfile',
148
- docker: 'Docker',
149
- nginx: 'Nginx',
150
- apache: 'Apache',
151
- ini: 'INI',
152
- toml: 'TOML',
153
- makefile: 'Makefile',
154
- diff: 'Diff',
155
- plaintext: 'Plain Text',
156
- text: 'Plain Text',
157
- mermaid: 'Mermaid'
158
- };
159
- return languageMap[language.toLowerCase()] || language.toUpperCase();
160
- }
161
104
  /**
162
105
  * Remove all copy buttons from code blocks in a container
163
106
  * Useful for cleanup when component is destroyed
@@ -1 +1 @@
1
- {"version":3,"file":"code-copy.extension.js","sourceRoot":"","sources":["../../../src/lib/extensions/code-copy.extension.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Code copy extension utilities\n *\n * This module provides utilities for adding copy-to-clipboard functionality\n * to code blocks. Unlike other marked extensions, this operates on the DOM\n * after rendering since it requires adding interactive elements.\n */\n\n/**\n * Options for the code copy functionality\n */\nexport interface CodeCopyOptions {\n /**\n * CSS class for the copy button\n * @default 'code-copy-btn'\n */\n buttonClass?: string;\n\n /**\n * CSS class for the toolbar container\n * @default 'code-toolbar'\n */\n toolbarClass?: string;\n\n /**\n * Icon HTML for the copy button\n * @default '<i class=\"fas fa-copy\"></i>'\n */\n copyIcon?: string;\n\n /**\n * Icon HTML for successful copy\n * @default '<i class=\"fas fa-check\"></i>'\n */\n successIcon?: string;\n\n /**\n * Icon HTML for failed copy\n * @default '<i class=\"fas fa-times\"></i>'\n */\n errorIcon?: string;\n\n /**\n * Tooltip text for the copy button\n * @default 'Copy code'\n */\n tooltipText?: string;\n\n /**\n * Duration in ms to show success/error state\n * @default 2000\n */\n feedbackDuration?: number;\n\n /**\n * Callback when code is successfully copied\n */\n onCopy?: (code: string) => void;\n\n /**\n * Callback when copy fails\n */\n onError?: (error: Error) => void;\n\n /**\n * Whether to show the language label in the toolbar\n * @default true\n */\n showLanguageLabel?: boolean;\n}\n\nconst DEFAULT_OPTIONS: Required<Omit<CodeCopyOptions, 'onCopy' | 'onError'>> & Pick<CodeCopyOptions, 'onCopy' | 'onError'> = {\n buttonClass: 'code-copy-btn',\n toolbarClass: 'code-toolbar',\n copyIcon: '<i class=\"fas fa-copy\"></i>',\n successIcon: '<i class=\"fas fa-check\"></i>',\n errorIcon: '<i class=\"fas fa-times\"></i>',\n tooltipText: 'Copy code',\n feedbackDuration: 2000,\n showLanguageLabel: true,\n onCopy: undefined,\n onError: undefined\n};\n\n/**\n * Add copy buttons to all code blocks within a container\n * @param container The DOM element containing code blocks\n * @param options Configuration options\n */\nexport function addCopyButtonsToCodeBlocks(\n container: HTMLElement,\n options?: CodeCopyOptions\n): void {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n\n const codeBlocks = container.querySelectorAll('pre > code');\n\n codeBlocks.forEach((codeBlock) => {\n const pre = codeBlock.parentElement;\n if (!pre) return;\n\n // Skip if already has toolbar\n if (pre.querySelector(`.${opts.toolbarClass}`)) return;\n\n // Create toolbar\n const toolbar = document.createElement('div');\n toolbar.className = opts.toolbarClass;\n\n // Add language label if enabled\n if (opts.showLanguageLabel) {\n const language = extractLanguageFromCode(codeBlock);\n if (language) {\n const label = document.createElement('span');\n label.className = 'code-language-label';\n label.textContent = formatLanguageName(language);\n toolbar.appendChild(label);\n }\n }\n\n // Create copy button\n const button = document.createElement('button');\n button.className = opts.buttonClass;\n button.innerHTML = opts.copyIcon;\n button.title = opts.tooltipText;\n button.type = 'button';\n button.setAttribute('aria-label', opts.tooltipText);\n\n // Add click handler\n button.addEventListener('click', async () => {\n const code = codeBlock.textContent || '';\n\n try {\n await navigator.clipboard.writeText(code);\n\n // Show success state\n button.innerHTML = opts.successIcon;\n button.classList.add('copied');\n\n // Call success callback\n opts.onCopy?.(code);\n\n // Reset after delay\n setTimeout(() => {\n button.innerHTML = opts.copyIcon;\n button.classList.remove('copied');\n }, opts.feedbackDuration);\n } catch (err) {\n console.error('Failed to copy code:', err);\n\n // Show error state\n button.innerHTML = opts.errorIcon;\n button.classList.add('error');\n\n // Call error callback\n opts.onError?.(err as Error);\n\n // Reset after delay\n setTimeout(() => {\n button.innerHTML = opts.copyIcon;\n button.classList.remove('error');\n }, opts.feedbackDuration);\n }\n });\n\n toolbar.appendChild(button);\n\n // Position the pre element for absolute positioning of toolbar\n pre.style.position = 'relative';\n pre.insertBefore(toolbar, pre.firstChild);\n });\n}\n\n/**\n * Extract the language class from a code element\n */\nfunction extractLanguageFromCode(codeElement: Element): string | null {\n const classes = codeElement.className.split(' ');\n\n for (const cls of classes) {\n if (cls.startsWith('language-')) {\n return cls.replace('language-', '');\n }\n }\n\n return null;\n}\n\n/**\n * Format a language identifier for display\n */\nfunction formatLanguageName(language: string): string {\n const languageMap: Record<string, string> = {\n js: 'JavaScript',\n ts: 'TypeScript',\n javascript: 'JavaScript',\n typescript: 'TypeScript',\n py: 'Python',\n python: 'Python',\n rb: 'Ruby',\n ruby: 'Ruby',\n cs: 'C#',\n csharp: 'C#',\n cpp: 'C++',\n 'c++': 'C++',\n html: 'HTML',\n css: 'CSS',\n scss: 'SCSS',\n sass: 'Sass',\n less: 'Less',\n json: 'JSON',\n yaml: 'YAML',\n yml: 'YAML',\n xml: 'XML',\n sql: 'SQL',\n bash: 'Bash',\n shell: 'Shell',\n sh: 'Shell',\n zsh: 'Zsh',\n powershell: 'PowerShell',\n ps1: 'PowerShell',\n md: 'Markdown',\n markdown: 'Markdown',\n graphql: 'GraphQL',\n gql: 'GraphQL',\n java: 'Java',\n kotlin: 'Kotlin',\n swift: 'Swift',\n go: 'Go',\n rust: 'Rust',\n php: 'PHP',\n r: 'R',\n dockerfile: 'Dockerfile',\n docker: 'Docker',\n nginx: 'Nginx',\n apache: 'Apache',\n ini: 'INI',\n toml: 'TOML',\n makefile: 'Makefile',\n diff: 'Diff',\n plaintext: 'Plain Text',\n text: 'Plain Text',\n mermaid: 'Mermaid'\n };\n\n return languageMap[language.toLowerCase()] || language.toUpperCase();\n}\n\n/**\n * Remove all copy buttons from code blocks in a container\n * Useful for cleanup when component is destroyed\n */\nexport function removeCopyButtonsFromCodeBlocks(\n container: HTMLElement,\n toolbarClass: string = 'code-toolbar'\n): void {\n const toolbars = container.querySelectorAll(`.${toolbarClass}`);\n toolbars.forEach((toolbar) => toolbar.remove());\n}\n"]}
1
+ {"version":3,"file":"code-copy.extension.js","sourceRoot":"","sources":["../../../src/lib/extensions/code-copy.extension.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Code copy extension utilities\n *\n * This module provides utilities for adding copy-to-clipboard functionality\n * to code blocks. Unlike other marked extensions, this operates on the DOM\n * after rendering since it requires adding interactive elements.\n */\nimport { formatLanguageName } from '@memberjunction/markdown-core';\n\n/**\n * Options for the code copy functionality\n */\nexport interface CodeCopyOptions {\n /**\n * CSS class for the copy button\n * @default 'code-copy-btn'\n */\n buttonClass?: string;\n\n /**\n * CSS class for the toolbar container\n * @default 'code-toolbar'\n */\n toolbarClass?: string;\n\n /**\n * Icon HTML for the copy button\n * @default '<i class=\"fas fa-copy\"></i>'\n */\n copyIcon?: string;\n\n /**\n * Icon HTML for successful copy\n * @default '<i class=\"fas fa-check\"></i>'\n */\n successIcon?: string;\n\n /**\n * Icon HTML for failed copy\n * @default '<i class=\"fas fa-times\"></i>'\n */\n errorIcon?: string;\n\n /**\n * Tooltip text for the copy button\n * @default 'Copy code'\n */\n tooltipText?: string;\n\n /**\n * Duration in ms to show success/error state\n * @default 2000\n */\n feedbackDuration?: number;\n\n /**\n * Callback when code is successfully copied\n */\n onCopy?: (code: string) => void;\n\n /**\n * Callback when copy fails\n */\n onError?: (error: Error) => void;\n\n /**\n * Whether to show the language label in the toolbar\n * @default true\n */\n showLanguageLabel?: boolean;\n}\n\nconst DEFAULT_OPTIONS: Required<Omit<CodeCopyOptions, 'onCopy' | 'onError'>> & Pick<CodeCopyOptions, 'onCopy' | 'onError'> = {\n buttonClass: 'code-copy-btn',\n toolbarClass: 'code-toolbar',\n copyIcon: '<i class=\"fas fa-copy\"></i>',\n successIcon: '<i class=\"fas fa-check\"></i>',\n errorIcon: '<i class=\"fas fa-times\"></i>',\n tooltipText: 'Copy code',\n feedbackDuration: 2000,\n showLanguageLabel: true,\n onCopy: undefined,\n onError: undefined\n};\n\n/**\n * Add copy buttons to all code blocks within a container\n * @param container The DOM element containing code blocks\n * @param options Configuration options\n */\nexport function addCopyButtonsToCodeBlocks(\n container: HTMLElement,\n options?: CodeCopyOptions\n): void {\n const opts = { ...DEFAULT_OPTIONS, ...options };\n\n const codeBlocks = container.querySelectorAll('pre > code');\n\n codeBlocks.forEach((codeBlock) => {\n const pre = codeBlock.parentElement;\n if (!pre) return;\n\n // Skip if already has toolbar\n if (pre.querySelector(`.${opts.toolbarClass}`)) return;\n\n // Create toolbar\n const toolbar = document.createElement('div');\n toolbar.className = opts.toolbarClass;\n\n // Add language label if enabled\n if (opts.showLanguageLabel) {\n const language = extractLanguageFromCode(codeBlock);\n if (language) {\n const label = document.createElement('span');\n label.className = 'code-language-label';\n label.textContent = formatLanguageName(language);\n toolbar.appendChild(label);\n }\n }\n\n // Create copy button\n const button = document.createElement('button');\n button.className = opts.buttonClass;\n button.innerHTML = opts.copyIcon;\n button.title = opts.tooltipText;\n button.type = 'button';\n button.setAttribute('aria-label', opts.tooltipText);\n\n // Add click handler\n button.addEventListener('click', async () => {\n const code = codeBlock.textContent || '';\n\n try {\n await navigator.clipboard.writeText(code);\n\n // Show success state\n button.innerHTML = opts.successIcon;\n button.classList.add('copied');\n\n // Call success callback\n opts.onCopy?.(code);\n\n // Reset after delay\n setTimeout(() => {\n button.innerHTML = opts.copyIcon;\n button.classList.remove('copied');\n }, opts.feedbackDuration);\n } catch (err) {\n console.error('Failed to copy code:', err);\n\n // Show error state\n button.innerHTML = opts.errorIcon;\n button.classList.add('error');\n\n // Call error callback\n opts.onError?.(err as Error);\n\n // Reset after delay\n setTimeout(() => {\n button.innerHTML = opts.copyIcon;\n button.classList.remove('error');\n }, opts.feedbackDuration);\n }\n });\n\n toolbar.appendChild(button);\n\n // Position the pre element for absolute positioning of toolbar\n pre.style.position = 'relative';\n pre.insertBefore(toolbar, pre.firstChild);\n });\n}\n\n/**\n * Extract the language class from a code element\n */\nfunction extractLanguageFromCode(codeElement: Element): string | null {\n const classes = codeElement.className.split(' ');\n\n for (const cls of classes) {\n if (cls.startsWith('language-')) {\n return cls.replace('language-', '');\n }\n }\n\n return null;\n}\n\n/**\n * Remove all copy buttons from code blocks in a container\n * Useful for cleanup when component is destroyed\n */\nexport function removeCopyButtonsFromCodeBlocks(\n container: HTMLElement,\n toolbarClass: string = 'code-toolbar'\n): void {\n const toolbars = container.querySelectorAll(`.${toolbarClass}`);\n toolbars.forEach((toolbar) => toolbar.remove());\n}\n"]}
@@ -1,73 +1,11 @@
1
- import { MarkedExtension } from 'marked';
2
1
  /**
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
- * Usage:
39
- * ```typescript
40
- * import { Marked } from 'marked';
41
- * import { createCollapsibleHeadingsExtension } from './collapsible-headings.extension';
42
- *
43
- * const marked = new Marked();
44
- * marked.use(createCollapsibleHeadingsExtension({ startLevel: 2 }));
45
- * ```
46
- *
47
- * The generated HTML structure (properly nested):
48
- * ```html
49
- * <div class="collapsible-section" data-level="2">
50
- * <div class="collapsible-heading-wrapper">
51
- * <h2 class="collapsible-heading" id="...">Parent Heading</h2>
52
- * </div>
53
- * <div class="collapsible-content">
54
- * <p>Content under parent...</p>
55
- * <div class="collapsible-section" data-level="3">
56
- * <div class="collapsible-heading-wrapper">
57
- * <h3 class="collapsible-heading" id="...">Child Heading</h3>
58
- * </div>
59
- * <div class="collapsible-content">
60
- * <p>Content under child...</p>
61
- * </div>
62
- * </div>
63
- * </div>
64
- * </div>
65
- * ```
2
+ * Web-only DOM helpers for collapsible heading sections.
66
3
  *
67
- * Note: The toggle button is added dynamically by the component after rendering
68
- * to avoid issues with Angular's HTML sanitizer.
4
+ * The framework-agnostic marked extension that produces the nested
5
+ * `<div class="collapsible-section">` HTML (`createCollapsibleHeadingsExtension`)
6
+ * now lives in `@memberjunction/markdown-core`. This file keeps the browser-only
7
+ * helpers that toggle the rendered sections in a live DOM.
69
8
  */
70
- export declare function createCollapsibleHeadingsExtension(options?: CollapsibleHeadingsOptions): MarkedExtension;
71
9
  /**
72
10
  * Helper function to toggle a collapsible section programmatically
73
11
  */
@@ -1 +1 @@
1
- {"version":3,"file":"collapsible-headings.extension.d.ts","sourceRoot":"","sources":["../../../src/lib/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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,wBAAgB,kCAAkC,CAChD,OAAO,CAAC,EAAE,0BAA0B,GACnC,eAAe,CA8BjB;AAuID;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,cAAc,EAAE,WAAW,GAAG,IAAI,CAS1E;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,WAAW,GAAG,IAAI,CAS9D;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,WAAW,GAAG,IAAI,CAShE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAgB/E"}
1
+ {"version":3,"file":"collapsible-headings.extension.d.ts","sourceRoot":"","sources":["../../../src/lib/extensions/collapsible-headings.extension.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,cAAc,EAAE,WAAW,GAAG,IAAI,CAS1E;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,WAAW,GAAG,IAAI,CAS9D;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,WAAW,GAAG,IAAI,CAShE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAgB/E"}
@@ -1,185 +1,11 @@
1
- const DEFAULT_OPTIONS = {
2
- startLevel: 2,
3
- defaultExpanded: true,
4
- autoExpandLevels: undefined,
5
- classPrefix: 'collapsible'
6
- };
7
1
  /**
8
- * Creates a marked extension that wraps heading sections in collapsible containers.
2
+ * Web-only DOM helpers for collapsible heading sections.
9
3
  *
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
- * Usage:
17
- * ```typescript
18
- * import { Marked } from 'marked';
19
- * import { createCollapsibleHeadingsExtension } from './collapsible-headings.extension';
20
- *
21
- * const marked = new Marked();
22
- * marked.use(createCollapsibleHeadingsExtension({ startLevel: 2 }));
23
- * ```
24
- *
25
- * The generated HTML structure (properly nested):
26
- * ```html
27
- * <div class="collapsible-section" data-level="2">
28
- * <div class="collapsible-heading-wrapper">
29
- * <h2 class="collapsible-heading" id="...">Parent Heading</h2>
30
- * </div>
31
- * <div class="collapsible-content">
32
- * <p>Content under parent...</p>
33
- * <div class="collapsible-section" data-level="3">
34
- * <div class="collapsible-heading-wrapper">
35
- * <h3 class="collapsible-heading" id="...">Child Heading</h3>
36
- * </div>
37
- * <div class="collapsible-content">
38
- * <p>Content under child...</p>
39
- * </div>
40
- * </div>
41
- * </div>
42
- * </div>
43
- * ```
44
- *
45
- * Note: The toggle button is added dynamically by the component after rendering
46
- * to avoid issues with Angular's HTML sanitizer.
47
- */
48
- export function createCollapsibleHeadingsExtension(options) {
49
- const opts = { ...DEFAULT_OPTIONS, ...options };
50
- return {
51
- renderer: {
52
- // Mark headings with a special marker that we'll process in postprocess
53
- heading(token) {
54
- const { depth, text } = token;
55
- const escapedText = text.toLowerCase().replace(/[^\w]+/g, '-');
56
- const id = `heading-${escapedText}`;
57
- // Only make headings collapsible at or below the start level
58
- if (depth >= opts.startLevel) {
59
- // Use a marker format that we'll parse in postprocess
60
- // Format: <!--COLLAPSIBLE_HEADING:level:id:text-->
61
- const marker = `<!--COLLAPSIBLE_HEADING:${depth}:${id}:${encodeURIComponent(text)}-->`;
62
- return `${marker}<h${depth} id="${id}">${text}</h${depth}>\n`;
63
- }
64
- // Regular heading rendering for levels above startLevel
65
- return `<h${depth} id="${id}">${text}</h${depth}>\n`;
66
- }
67
- },
68
- hooks: {
69
- postprocess(html) {
70
- return restructureToNestedSections(html, opts);
71
- }
72
- }
73
- };
74
- }
75
- /**
76
- * Restructures flat HTML with heading markers into properly nested collapsible sections.
77
- * This is the key function that creates the hierarchical structure needed for
78
- * parent collapse to affect children.
4
+ * The framework-agnostic marked extension that produces the nested
5
+ * `<div class="collapsible-section">` HTML (`createCollapsibleHeadingsExtension`)
6
+ * now lives in `@memberjunction/markdown-core`. This file keeps the browser-only
7
+ * helpers that toggle the rendered sections in a live DOM.
79
8
  */
80
- function restructureToNestedSections(html, opts) {
81
- // Parse the HTML to find all content chunks and headings
82
- const markerRegex = /<!--COLLAPSIBLE_HEADING:(\d):([^:]+):([^>]+)-->/g;
83
- // Split HTML by markers while capturing the markers
84
- const parts = [];
85
- let lastIndex = 0;
86
- let match;
87
- while ((match = markerRegex.exec(html)) !== null) {
88
- // Add content before this marker
89
- if (match.index > lastIndex) {
90
- const content = html.slice(lastIndex, match.index);
91
- if (content.trim()) {
92
- parts.push({ type: 'content', content });
93
- }
94
- }
95
- // Add the heading marker info
96
- parts.push({
97
- type: 'heading',
98
- content: '', // Will be filled with the actual h tag
99
- level: parseInt(match[1], 10),
100
- id: match[2],
101
- text: decodeURIComponent(match[3])
102
- });
103
- lastIndex = match.index + match[0].length;
104
- }
105
- // Add remaining content after last marker
106
- if (lastIndex < html.length) {
107
- const content = html.slice(lastIndex);
108
- if (content.trim()) {
109
- parts.push({ type: 'content', content });
110
- }
111
- }
112
- // If no collapsible headings, return original HTML
113
- if (!parts.some(p => p.type === 'heading')) {
114
- return html;
115
- }
116
- // Now build the nested structure
117
- return buildNestedStructure(parts, opts);
118
- }
119
- /**
120
- * Determine if a heading level should start expanded based on options
121
- */
122
- function shouldLevelBeExpanded(level, opts) {
123
- // If autoExpandLevels is specified, use it
124
- if (opts.autoExpandLevels !== undefined) {
125
- return opts.autoExpandLevels.includes(level);
126
- }
127
- // Otherwise fall back to defaultExpanded
128
- return opts.defaultExpanded;
129
- }
130
- /**
131
- * Builds the nested HTML structure from the parsed parts.
132
- */
133
- function buildNestedStructure(parts, opts) {
134
- const result = [];
135
- // Stack tracks open sections: { level, hasContent }
136
- const sectionStack = [];
137
- for (let i = 0; i < parts.length; i++) {
138
- const part = parts[i];
139
- if (part.type === 'content') {
140
- // Regular content - just add it
141
- result.push(part.content);
142
- }
143
- else if (part.type === 'heading' && part.level !== undefined) {
144
- const level = part.level;
145
- // Close any sections at same or higher level (lower number = higher level)
146
- while (sectionStack.length > 0 && sectionStack[sectionStack.length - 1].level >= level) {
147
- // Close the content div and section div
148
- result.push('</div></div>');
149
- sectionStack.pop();
150
- }
151
- // Determine expanded state for this specific level
152
- const isExpanded = shouldLevelBeExpanded(level, opts);
153
- const expandedClass = isExpanded ? '' : ' collapsed';
154
- // Extract the actual h tag from the next content piece if it starts with it
155
- let headingHtml = `<h${level} class="${opts.classPrefix}-heading" id="${part.id}">${part.text}</h${level}>`;
156
- // Look ahead to find and remove the actual h tag from content
157
- if (i + 1 < parts.length && parts[i + 1].type === 'content') {
158
- const nextContent = parts[i + 1].content;
159
- const hTagRegex = new RegExp(`<h${level}[^>]*id="${part.id}"[^>]*>[^<]*</h${level}>\\s*`);
160
- const hTagMatch = nextContent.match(hTagRegex);
161
- if (hTagMatch) {
162
- // Use the actual h tag and remove it from content
163
- headingHtml = hTagMatch[0].replace(`<h${level}`, `<h${level} class="${opts.classPrefix}-heading"`);
164
- parts[i + 1].content = nextContent.replace(hTagRegex, '');
165
- }
166
- }
167
- // Open new section
168
- result.push(`<div class="${opts.classPrefix}-section${expandedClass}" data-level="${level}">`);
169
- result.push(`<div class="${opts.classPrefix}-heading-wrapper">`);
170
- result.push(headingHtml);
171
- result.push('</div>');
172
- result.push(`<div class="${opts.classPrefix}-content">`);
173
- sectionStack.push({ level });
174
- }
175
- }
176
- // Close any remaining open sections
177
- while (sectionStack.length > 0) {
178
- result.push('</div></div>');
179
- sectionStack.pop();
180
- }
181
- return result.join('');
182
- }
183
9
  /**
184
10
  * Helper function to toggle a collapsible section programmatically
185
11
  */
@@ -1 +1 @@
1
- {"version":3,"file":"collapsible-headings.extension.js","sourceRoot":"","sources":["../../../src/lib/extensions/collapsible-headings.extension.ts"],"names":[],"mappings":"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","sourcesContent":["import { MarkedExtension, Tokens } from 'marked';\n\n/**\n * Options for the collapsible headings extension\n */\nexport interface CollapsibleHeadingsOptions {\n /**\n * The minimum heading level to make collapsible (1-6)\n * Headings at this level and below will be collapsible\n * @default 2\n */\n startLevel?: 1 | 2 | 3 | 4 | 5 | 6;\n\n /**\n * Whether sections should be expanded by default\n * @default true\n */\n defaultExpanded?: boolean;\n\n /**\n * Specify which heading levels should start expanded.\n * Takes precedence over defaultExpanded for specified levels.\n * @default undefined (uses defaultExpanded for all levels)\n */\n autoExpandLevels?: number[];\n\n /**\n * CSS class prefix for generated elements\n * @default 'collapsible'\n */\n classPrefix?: string;\n}\n\ninterface ResolvedOptions {\n startLevel: 1 | 2 | 3 | 4 | 5 | 6;\n defaultExpanded: boolean;\n autoExpandLevels?: number[];\n classPrefix: string;\n}\n\nconst DEFAULT_OPTIONS: ResolvedOptions = {\n startLevel: 2,\n defaultExpanded: true,\n autoExpandLevels: undefined,\n classPrefix: 'collapsible'\n};\n\n/**\n * Creates a marked extension that wraps heading sections in collapsible containers.\n *\n * This extension transforms headings into clickable toggles that can expand/collapse\n * the content that follows them (until the next heading of equal or higher level).\n *\n * The key innovation is that child sections are properly NESTED inside parent sections,\n * so collapsing a parent will hide all its children.\n *\n * Usage:\n * ```typescript\n * import { Marked } from 'marked';\n * import { createCollapsibleHeadingsExtension } from './collapsible-headings.extension';\n *\n * const marked = new Marked();\n * marked.use(createCollapsibleHeadingsExtension({ startLevel: 2 }));\n * ```\n *\n * The generated HTML structure (properly nested):\n * ```html\n * <div class=\"collapsible-section\" data-level=\"2\">\n * <div class=\"collapsible-heading-wrapper\">\n * <h2 class=\"collapsible-heading\" id=\"...\">Parent Heading</h2>\n * </div>\n * <div class=\"collapsible-content\">\n * <p>Content under parent...</p>\n * <div class=\"collapsible-section\" data-level=\"3\">\n * <div class=\"collapsible-heading-wrapper\">\n * <h3 class=\"collapsible-heading\" id=\"...\">Child Heading</h3>\n * </div>\n * <div class=\"collapsible-content\">\n * <p>Content under child...</p>\n * </div>\n * </div>\n * </div>\n * </div>\n * ```\n *\n * Note: The toggle button is added dynamically by the component after rendering\n * to avoid issues with Angular's HTML sanitizer.\n */\nexport function createCollapsibleHeadingsExtension(\n options?: CollapsibleHeadingsOptions\n): MarkedExtension {\n const opts: ResolvedOptions = { ...DEFAULT_OPTIONS, ...options };\n\n return {\n renderer: {\n // Mark headings with a special marker that we'll process in postprocess\n heading(token: Tokens.Heading): string {\n const { depth, text } = token;\n const escapedText = text.toLowerCase().replace(/[^\\w]+/g, '-');\n const id = `heading-${escapedText}`;\n\n // Only make headings collapsible at or below the start level\n if (depth >= opts.startLevel) {\n // Use a marker format that we'll parse in postprocess\n // Format: <!--COLLAPSIBLE_HEADING:level:id:text-->\n const marker = `<!--COLLAPSIBLE_HEADING:${depth}:${id}:${encodeURIComponent(text)}-->`;\n return `${marker}<h${depth} id=\"${id}\">${text}</h${depth}>\\n`;\n }\n\n // Regular heading rendering for levels above startLevel\n return `<h${depth} id=\"${id}\">${text}</h${depth}>\\n`;\n }\n },\n\n hooks: {\n postprocess(html: string): string {\n return restructureToNestedSections(html, opts);\n }\n }\n };\n}\n\n/**\n * Restructures flat HTML with heading markers into properly nested collapsible sections.\n * This is the key function that creates the hierarchical structure needed for\n * parent collapse to affect children.\n */\nfunction restructureToNestedSections(\n html: string,\n opts: ResolvedOptions\n): string {\n // Parse the HTML to find all content chunks and headings\n const markerRegex = /<!--COLLAPSIBLE_HEADING:(\\d):([^:]+):([^>]+)-->/g;\n\n // Split HTML by markers while capturing the markers\n const parts: Array<{ type: 'content' | 'heading'; content: string; level?: number; id?: string; text?: string }> = [];\n let lastIndex = 0;\n let match;\n\n while ((match = markerRegex.exec(html)) !== null) {\n // Add content before this marker\n if (match.index > lastIndex) {\n const content = html.slice(lastIndex, match.index);\n if (content.trim()) {\n parts.push({ type: 'content', content });\n }\n }\n\n // Add the heading marker info\n parts.push({\n type: 'heading',\n content: '', // Will be filled with the actual h tag\n level: parseInt(match[1], 10),\n id: match[2],\n text: decodeURIComponent(match[3])\n });\n\n lastIndex = match.index + match[0].length;\n }\n\n // Add remaining content after last marker\n if (lastIndex < html.length) {\n const content = html.slice(lastIndex);\n if (content.trim()) {\n parts.push({ type: 'content', content });\n }\n }\n\n // If no collapsible headings, return original HTML\n if (!parts.some(p => p.type === 'heading')) {\n return html;\n }\n\n // Now build the nested structure\n return buildNestedStructure(parts, opts);\n}\n\n/**\n * Determine if a heading level should start expanded based on options\n */\nfunction shouldLevelBeExpanded(level: number, opts: ResolvedOptions): boolean {\n // If autoExpandLevels is specified, use it\n if (opts.autoExpandLevels !== undefined) {\n return opts.autoExpandLevels.includes(level);\n }\n // Otherwise fall back to defaultExpanded\n return opts.defaultExpanded;\n}\n\n/**\n * Builds the nested HTML structure from the parsed parts.\n */\nfunction buildNestedStructure(\n parts: Array<{ type: 'content' | 'heading'; content: string; level?: number; id?: string; text?: string }>,\n opts: ResolvedOptions\n): string {\n const result: string[] = [];\n // Stack tracks open sections: { level, hasContent }\n const sectionStack: Array<{ level: number }> = [];\n\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i];\n\n if (part.type === 'content') {\n // Regular content - just add it\n result.push(part.content);\n } else if (part.type === 'heading' && part.level !== undefined) {\n const level = part.level;\n\n // Close any sections at same or higher level (lower number = higher level)\n while (sectionStack.length > 0 && sectionStack[sectionStack.length - 1].level >= level) {\n // Close the content div and section div\n result.push('</div></div>');\n sectionStack.pop();\n }\n\n // Determine expanded state for this specific level\n const isExpanded = shouldLevelBeExpanded(level, opts);\n const expandedClass = isExpanded ? '' : ' collapsed';\n\n // Extract the actual h tag from the next content piece if it starts with it\n let headingHtml = `<h${level} class=\"${opts.classPrefix}-heading\" id=\"${part.id}\">${part.text}</h${level}>`;\n\n // Look ahead to find and remove the actual h tag from content\n if (i + 1 < parts.length && parts[i + 1].type === 'content') {\n const nextContent = parts[i + 1].content;\n const hTagRegex = new RegExp(`<h${level}[^>]*id=\"${part.id}\"[^>]*>[^<]*</h${level}>\\\\s*`);\n const hTagMatch = nextContent.match(hTagRegex);\n if (hTagMatch) {\n // Use the actual h tag and remove it from content\n headingHtml = hTagMatch[0].replace(`<h${level}`, `<h${level} class=\"${opts.classPrefix}-heading\"`);\n parts[i + 1].content = nextContent.replace(hTagRegex, '');\n }\n }\n\n // Open new section\n result.push(`<div class=\"${opts.classPrefix}-section${expandedClass}\" data-level=\"${level}\">`);\n result.push(`<div class=\"${opts.classPrefix}-heading-wrapper\">`);\n result.push(headingHtml);\n result.push('</div>');\n result.push(`<div class=\"${opts.classPrefix}-content\">`);\n\n sectionStack.push({ level });\n }\n }\n\n // Close any remaining open sections\n while (sectionStack.length > 0) {\n result.push('</div></div>');\n sectionStack.pop();\n }\n\n return result.join('');\n}\n\n/**\n * Helper function to toggle a collapsible section programmatically\n */\nexport function toggleCollapsibleSection(sectionElement: HTMLElement): void {\n const isCollapsed = sectionElement.classList.contains('collapsed');\n const toggle = sectionElement.querySelector('.collapsible-toggle');\n\n sectionElement.classList.toggle('collapsed');\n\n if (toggle) {\n toggle.setAttribute('aria-expanded', String(isCollapsed));\n }\n}\n\n/**\n * Expand all collapsible sections in a container\n */\nexport function expandAllSections(container: HTMLElement): void {\n const sections = container.querySelectorAll('.collapsible-section.collapsed');\n sections.forEach((section) => {\n section.classList.remove('collapsed');\n const toggle = section.querySelector('.collapsible-toggle');\n if (toggle) {\n toggle.setAttribute('aria-expanded', 'true');\n }\n });\n}\n\n/**\n * Collapse all collapsible sections in a container\n */\nexport function collapseAllSections(container: HTMLElement): void {\n const sections = container.querySelectorAll('.collapsible-section:not(.collapsed)');\n sections.forEach((section) => {\n section.classList.add('collapsed');\n const toggle = section.querySelector('.collapsible-toggle');\n if (toggle) {\n toggle.setAttribute('aria-expanded', 'false');\n }\n });\n}\n\n/**\n * Expand sections to reveal a specific heading by ID\n */\nexport function expandToHeading(container: HTMLElement, headingId: string): void {\n const heading = container.querySelector(`#${headingId}`);\n if (!heading) return;\n\n // Find all ancestor collapsible sections and expand them\n let current: HTMLElement | null = heading.closest('.collapsible-section');\n while (current) {\n if (current.classList.contains('collapsed')) {\n current.classList.remove('collapsed');\n const toggle = current.querySelector(':scope > .collapsible-heading-wrapper .collapsible-toggle');\n if (toggle) {\n toggle.setAttribute('aria-expanded', 'true');\n }\n }\n current = current.parentElement?.closest('.collapsible-section') || null;\n }\n}\n"]}
1
+ {"version":3,"file":"collapsible-headings.extension.js","sourceRoot":"","sources":["../../../src/lib/extensions/collapsible-headings.extension.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,MAAM,UAAU,wBAAwB,CAAC,cAA2B;IAClE,MAAM,WAAW,GAAG,cAAc,CAAC,SAAS,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IACnE,MAAM,MAAM,GAAG,cAAc,CAAC,aAAa,CAAC,qBAAqB,CAAC,CAAC;IAEnE,cAAc,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAE7C,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,CAAC,YAAY,CAAC,eAAe,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAsB;IACtD,MAAM,QAAQ,GAAG,SAAS,CAAC,gBAAgB,CAAC,gCAAgC,CAAC,CAAC;IAC9E,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC3B,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QACtC,MAAM,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,qBAAqB,CAAC,CAAC;QAC5D,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,CAAC,YAAY,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,SAAsB;IACxD,MAAM,QAAQ,GAAG,SAAS,CAAC,gBAAgB,CAAC,sCAAsC,CAAC,CAAC;IACpF,QAAQ,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC3B,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACnC,MAAM,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,qBAAqB,CAAC,CAAC;QAC5D,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;QAChD,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,eAAe,CAAC,SAAsB,EAAE,SAAiB;IACvE,MAAM,OAAO,GAAG,SAAS,CAAC,aAAa,CAAC,IAAI,SAAS,EAAE,CAAC,CAAC;IACzD,IAAI,CAAC,OAAO;QAAE,OAAO;IAErB,yDAAyD;IACzD,IAAI,OAAO,GAAuB,OAAO,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC1E,OAAO,OAAO,EAAE,CAAC;QACf,IAAI,OAAO,CAAC,SAAS,CAAC,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC;YAC5C,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;YACtC,MAAM,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,2DAA2D,CAAC,CAAC;YAClG,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,CAAC,YAAY,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;YAC/C,CAAC;QACH,CAAC;QACD,OAAO,GAAG,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,sBAAsB,CAAC,IAAI,IAAI,CAAC;IAC3E,CAAC;AACH,CAAC","sourcesContent":["/**\n * Web-only DOM helpers for collapsible heading sections.\n *\n * The framework-agnostic marked extension that produces the nested\n * `<div class=\"collapsible-section\">` HTML (`createCollapsibleHeadingsExtension`)\n * now lives in `@memberjunction/markdown-core`. This file keeps the browser-only\n * helpers that toggle the rendered sections in a live DOM.\n */\n\n/**\n * Helper function to toggle a collapsible section programmatically\n */\nexport function toggleCollapsibleSection(sectionElement: HTMLElement): void {\n const isCollapsed = sectionElement.classList.contains('collapsed');\n const toggle = sectionElement.querySelector('.collapsible-toggle');\n\n sectionElement.classList.toggle('collapsed');\n\n if (toggle) {\n toggle.setAttribute('aria-expanded', String(isCollapsed));\n }\n}\n\n/**\n * Expand all collapsible sections in a container\n */\nexport function expandAllSections(container: HTMLElement): void {\n const sections = container.querySelectorAll('.collapsible-section.collapsed');\n sections.forEach((section) => {\n section.classList.remove('collapsed');\n const toggle = section.querySelector('.collapsible-toggle');\n if (toggle) {\n toggle.setAttribute('aria-expanded', 'true');\n }\n });\n}\n\n/**\n * Collapse all collapsible sections in a container\n */\nexport function collapseAllSections(container: HTMLElement): void {\n const sections = container.querySelectorAll('.collapsible-section:not(.collapsed)');\n sections.forEach((section) => {\n section.classList.add('collapsed');\n const toggle = section.querySelector('.collapsible-toggle');\n if (toggle) {\n toggle.setAttribute('aria-expanded', 'false');\n }\n });\n}\n\n/**\n * Expand sections to reveal a specific heading by ID\n */\nexport function expandToHeading(container: HTMLElement, headingId: string): void {\n const heading = container.querySelector(`#${headingId}`);\n if (!heading) return;\n\n // Find all ancestor collapsible sections and expand them\n let current: HTMLElement | null = heading.closest('.collapsible-section');\n while (current) {\n if (current.classList.contains('collapsed')) {\n current.classList.remove('collapsed');\n const toggle = current.querySelector(':scope > .collapsible-heading-wrapper .collapsible-toggle');\n if (toggle) {\n toggle.setAttribute('aria-expanded', 'true');\n }\n }\n current = current.parentElement?.closest('.collapsible-section') || null;\n }\n}\n"]}
@@ -1,34 +1,11 @@
1
- import { MarkedExtension } from 'marked';
2
1
  /**
3
- * Creates a marked extension that renders SVG code blocks as actual SVG images.
2
+ * Web-only DOM utilities that complement the SVG renderer marked extension.
4
3
  *
5
- * When encountering a code block with language "svg", this extension will
6
- * render it as an actual SVG element instead of showing the code.
7
- *
8
- * This is useful for:
9
- * - UX mockups and wireframes
10
- * - Diagrams and illustrations
11
- * - Icons and simple graphics
12
- * - Any visual content that can be expressed as SVG
13
- *
14
- * Usage in markdown:
15
- * ```svg
16
- * <svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
17
- * <circle cx="50" cy="50" r="40" fill="blue"/>
18
- * </svg>
19
- * ```
20
- *
21
- * The generated HTML structure:
22
- * ```html
23
- * <div class="svg-rendered">
24
- * <svg ...>...</svg>
25
- * </div>
26
- * ```
27
- *
28
- * Security note: SVG content is rendered as-is. Make sure to only render
29
- * trusted SVG content or enable sanitization if rendering user-provided content.
4
+ * The framework-agnostic SVG tokenizer/renderer (`createSvgRendererExtension`,
5
+ * `isSvgContent`) now lives in `@memberjunction/markdown-core`. This file keeps
6
+ * the browser-only sanitization helper, which operates on a live DOM subtree
7
+ * after the SVG has been inserted into the page.
30
8
  */
31
- export declare function createSvgRendererExtension(): MarkedExtension;
32
9
  /**
33
10
  * Helper function to sanitize SVG content by removing potentially dangerous elements.
34
11
  * Call this on the container element after rendering if you need additional security.
@@ -1 +1 @@
1
- {"version":3,"file":"svg-renderer.extension.d.ts","sourceRoot":"","sources":["../../../src/lib/extensions/svg-renderer.extension.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,0BAA0B,IAAI,eAAe,CAmC5D;AAiBD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,WAAW,GAAG,IAAI,CAgD/D"}
1
+ {"version":3,"file":"svg-renderer.extension.d.ts","sourceRoot":"","sources":["../../../src/lib/extensions/svg-renderer.extension.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,WAAW,GAAG,IAAI,CAgD/D"}
@@ -1,78 +1,11 @@
1
1
  /**
2
- * Creates a marked extension that renders SVG code blocks as actual SVG images.
2
+ * Web-only DOM utilities that complement the SVG renderer marked extension.
3
3
  *
4
- * When encountering a code block with language "svg", this extension will
5
- * render it as an actual SVG element instead of showing the code.
6
- *
7
- * This is useful for:
8
- * - UX mockups and wireframes
9
- * - Diagrams and illustrations
10
- * - Icons and simple graphics
11
- * - Any visual content that can be expressed as SVG
12
- *
13
- * Usage in markdown:
14
- * ```svg
15
- * <svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
16
- * <circle cx="50" cy="50" r="40" fill="blue"/>
17
- * </svg>
18
- * ```
19
- *
20
- * The generated HTML structure:
21
- * ```html
22
- * <div class="svg-rendered">
23
- * <svg ...>...</svg>
24
- * </div>
25
- * ```
26
- *
27
- * Security note: SVG content is rendered as-is. Make sure to only render
28
- * trusted SVG content or enable sanitization if rendering user-provided content.
4
+ * The framework-agnostic SVG tokenizer/renderer (`createSvgRendererExtension`,
5
+ * `isSvgContent`) now lives in `@memberjunction/markdown-core`. This file keeps
6
+ * the browser-only sanitization helper, which operates on a live DOM subtree
7
+ * after the SVG has been inserted into the page.
29
8
  */
30
- export function createSvgRendererExtension() {
31
- return {
32
- extensions: [{
33
- name: 'svgCodeBlock',
34
- level: 'block',
35
- start(src) {
36
- // Look for ```svg at the start of the source
37
- const match = src.match(/^```svg\b/);
38
- return match ? match.index : undefined;
39
- },
40
- tokenizer(src) {
41
- // Match ```svg code blocks
42
- const rule = /^```svg\n([\s\S]*?)```(?:\n|$)/;
43
- const match = rule.exec(src);
44
- if (match) {
45
- const svgContent = match[1].trim();
46
- // Only process if it looks like valid SVG
47
- if (isSvgContent(svgContent)) {
48
- return {
49
- type: 'svgCodeBlock',
50
- raw: match[0],
51
- svgContent: svgContent
52
- };
53
- }
54
- }
55
- return undefined;
56
- },
57
- renderer(token) {
58
- const svgToken = token;
59
- return `<div class="svg-rendered">${svgToken.svgContent}</div>\n`;
60
- }
61
- }]
62
- };
63
- }
64
- /**
65
- * Basic validation to check if content appears to be SVG.
66
- * This is a simple check - it doesn't fully validate SVG syntax.
67
- */
68
- function isSvgContent(content) {
69
- // Check if it starts with <svg and contains closing </svg>
70
- const startsWithSvg = content.toLowerCase().startsWith('<svg');
71
- const hasSvgClosing = content.toLowerCase().includes('</svg>');
72
- // Also accept self-closing SVG (rare but valid)
73
- const isSelfClosing = content.toLowerCase().match(/<svg[^>]*\/>/);
74
- return (startsWithSvg && hasSvgClosing) || !!isSelfClosing;
75
- }
76
9
  /**
77
10
  * Helper function to sanitize SVG content by removing potentially dangerous elements.
78
11
  * Call this on the container element after rendering if you need additional security.
@@ -1 +1 @@
1
- {"version":3,"file":"svg-renderer.extension.js","sourceRoot":"","sources":["../../../src/lib/extensions/svg-renderer.extension.ts"],"names":[],"mappings":"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","sourcesContent":["import { MarkedExtension } from 'marked';\n\n/**\n * Creates a marked extension that renders SVG code blocks as actual SVG images.\n *\n * When encountering a code block with language \"svg\", this extension will\n * render it as an actual SVG element instead of showing the code.\n *\n * This is useful for:\n * - UX mockups and wireframes\n * - Diagrams and illustrations\n * - Icons and simple graphics\n * - Any visual content that can be expressed as SVG\n *\n * Usage in markdown:\n * ```svg\n * <svg width=\"100\" height=\"100\" xmlns=\"http://www.w3.org/2000/svg\">\n * <circle cx=\"50\" cy=\"50\" r=\"40\" fill=\"blue\"/>\n * </svg>\n * ```\n *\n * The generated HTML structure:\n * ```html\n * <div class=\"svg-rendered\">\n * <svg ...>...</svg>\n * </div>\n * ```\n *\n * Security note: SVG content is rendered as-is. Make sure to only render\n * trusted SVG content or enable sanitization if rendering user-provided content.\n */\nexport function createSvgRendererExtension(): MarkedExtension {\n return {\n extensions: [{\n name: 'svgCodeBlock',\n level: 'block',\n start(src: string) {\n // Look for ```svg at the start of the source\n const match = src.match(/^```svg\\b/);\n return match ? match.index : undefined;\n },\n tokenizer(src: string) {\n // Match ```svg code blocks\n const rule = /^```svg\\n([\\s\\S]*?)```(?:\\n|$)/;\n const match = rule.exec(src);\n\n if (match) {\n const svgContent = match[1].trim();\n\n // Only process if it looks like valid SVG\n if (isSvgContent(svgContent)) {\n return {\n type: 'svgCodeBlock',\n raw: match[0],\n svgContent: svgContent\n };\n }\n }\n return undefined;\n },\n renderer(token) {\n const svgToken = token as unknown as { svgContent: string };\n return `<div class=\"svg-rendered\">${svgToken.svgContent}</div>\\n`;\n }\n }]\n };\n}\n\n/**\n * Basic validation to check if content appears to be SVG.\n * This is a simple check - it doesn't fully validate SVG syntax.\n */\nfunction isSvgContent(content: string): boolean {\n // Check if it starts with <svg and contains closing </svg>\n const startsWithSvg = content.toLowerCase().startsWith('<svg');\n const hasSvgClosing = content.toLowerCase().includes('</svg>');\n\n // Also accept self-closing SVG (rare but valid)\n const isSelfClosing = content.toLowerCase().match(/<svg[^>]*\\/>/);\n\n return (startsWithSvg && hasSvgClosing) || !!isSelfClosing;\n}\n\n/**\n * Helper function to sanitize SVG content by removing potentially dangerous elements.\n * Call this on the container element after rendering if you need additional security.\n *\n * @param container The DOM element containing rendered SVG\n */\nexport function sanitizeSvgContent(container: HTMLElement): void {\n // Remove script elements\n const scripts = container.querySelectorAll('script');\n scripts.forEach(script => script.remove());\n\n // Remove event handlers from all elements\n const allElements = container.querySelectorAll('*');\n allElements.forEach(el => {\n // Remove common event handler attributes\n const dangerousAttrs = [\n 'onload', 'onerror', 'onclick', 'onmouseover', 'onmouseout',\n 'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset',\n 'onkeydown', 'onkeyup', 'onkeypress'\n ];\n\n dangerousAttrs.forEach(attr => {\n if (el.hasAttribute(attr)) {\n el.removeAttribute(attr);\n }\n });\n\n // Remove javascript: URLs from href/xlink:href\n if (el.hasAttribute('href')) {\n const href = el.getAttribute('href') || '';\n if (href.toLowerCase().startsWith('javascript:')) {\n el.removeAttribute('href');\n }\n }\n if (el.hasAttribute('xlink:href')) {\n const href = el.getAttribute('xlink:href') || '';\n if (href.toLowerCase().startsWith('javascript:')) {\n el.removeAttribute('xlink:href');\n }\n }\n });\n\n // Remove foreignObject elements (can contain HTML/scripts)\n const foreignObjects = container.querySelectorAll('foreignObject');\n foreignObjects.forEach(fo => fo.remove());\n\n // Remove use elements pointing to external resources\n const useElements = container.querySelectorAll('use');\n useElements.forEach(use => {\n const href = use.getAttribute('href') || use.getAttribute('xlink:href') || '';\n if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//')) {\n use.remove();\n }\n });\n}\n"]}
1
+ {"version":3,"file":"svg-renderer.extension.js","sourceRoot":"","sources":["../../../src/lib/extensions/svg-renderer.extension.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,SAAsB;IACvD,yBAAyB;IACzB,MAAM,OAAO,GAAG,SAAS,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IACrD,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IAE3C,0CAA0C;IAC1C,MAAM,WAAW,GAAG,SAAS,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC;IACpD,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE;QACvB,yCAAyC;QACzC,MAAM,cAAc,GAAG;YACrB,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY;YAC3D,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS;YACtD,WAAW,EAAE,SAAS,EAAE,YAAY;SACrC,CAAC;QAEF,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;YAC5B,IAAI,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC1B,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,+CAA+C;QAC/C,IAAI,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;YAC3C,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;gBACjD,EAAE,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;QACD,IAAI,EAAE,CAAC,YAAY,CAAC,YAAY,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;YACjD,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;gBACjD,EAAE,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC;YACnC,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,2DAA2D;IAC3D,MAAM,cAAc,GAAG,SAAS,CAAC,gBAAgB,CAAC,eAAe,CAAC,CAAC;IACnE,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;IAE1C,qDAAqD;IACrD,MAAM,WAAW,GAAG,SAAS,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;IACtD,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE;QACxB,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,IAAI,GAAG,CAAC,YAAY,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;QAC9E,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YACvF,GAAG,CAAC,MAAM,EAAE,CAAC;QACf,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["/**\n * Web-only DOM utilities that complement the SVG renderer marked extension.\n *\n * The framework-agnostic SVG tokenizer/renderer (`createSvgRendererExtension`,\n * `isSvgContent`) now lives in `@memberjunction/markdown-core`. This file keeps\n * the browser-only sanitization helper, which operates on a live DOM subtree\n * after the SVG has been inserted into the page.\n */\n\n/**\n * Helper function to sanitize SVG content by removing potentially dangerous elements.\n * Call this on the container element after rendering if you need additional security.\n *\n * @param container The DOM element containing rendered SVG\n */\nexport function sanitizeSvgContent(container: HTMLElement): void {\n // Remove script elements\n const scripts = container.querySelectorAll('script');\n scripts.forEach(script => script.remove());\n\n // Remove event handlers from all elements\n const allElements = container.querySelectorAll('*');\n allElements.forEach(el => {\n // Remove common event handler attributes\n const dangerousAttrs = [\n 'onload', 'onerror', 'onclick', 'onmouseover', 'onmouseout',\n 'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset',\n 'onkeydown', 'onkeyup', 'onkeypress'\n ];\n\n dangerousAttrs.forEach(attr => {\n if (el.hasAttribute(attr)) {\n el.removeAttribute(attr);\n }\n });\n\n // Remove javascript: URLs from href/xlink:href\n if (el.hasAttribute('href')) {\n const href = el.getAttribute('href') || '';\n if (href.toLowerCase().startsWith('javascript:')) {\n el.removeAttribute('href');\n }\n }\n if (el.hasAttribute('xlink:href')) {\n const href = el.getAttribute('xlink:href') || '';\n if (href.toLowerCase().startsWith('javascript:')) {\n el.removeAttribute('xlink:href');\n }\n }\n });\n\n // Remove foreignObject elements (can contain HTML/scripts)\n const foreignObjects = container.querySelectorAll('foreignObject');\n foreignObjects.forEach(fo => fo.remove());\n\n // Remove use elements pointing to external resources\n const useElements = container.querySelectorAll('use');\n useElements.forEach(use => {\n const href = use.getAttribute('href') || use.getAttribute('xlink:href') || '';\n if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//')) {\n use.remove();\n }\n });\n}\n"]}