@limetech/lime-elements 40.5.4 → 40.6.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/CHANGELOG.md +11 -0
- package/dist/cjs/{index-ETPz91V5.js → index-zZvs6xK-.js} +9382 -9504
- package/dist/cjs/lime-elements.cjs.js +1 -1
- package/dist/cjs/limel-file-viewer.cjs.entry.js +1 -1
- package/dist/cjs/limel-markdown.cjs.entry.js +28 -4
- package/dist/cjs/limel-prosemirror-adapter.cjs.entry.js +2 -2
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/cjs/markdown-exporter-9p7d3Q76.js +6210 -0
- package/dist/cjs/{markdown-parser-YH5HSSgF.js → markdown-parser-Cgv5Y9Z4.js} +579 -848
- package/dist/collection/components/markdown/markdown-exporter.js +186 -0
- package/dist/collection/components/markdown/markdown-parser.js +2 -3
- package/dist/collection/components/markdown/markdown.js +54 -2
- package/dist/esm/{index-CbciMfiU.js → index-BN64nlbO.js} +9369 -9505
- package/dist/esm/lime-elements.js +1 -1
- package/dist/esm/limel-file-viewer.entry.js +1 -1
- package/dist/esm/limel-markdown.entry.js +28 -4
- package/dist/esm/limel-prosemirror-adapter.entry.js +2 -2
- package/dist/esm/loader.js +1 -1
- package/dist/esm/markdown-exporter--3_MaqwD.js +6208 -0
- package/dist/esm/{markdown-parser-DrxxGLF5.js → markdown-parser-DCf-3dKZ.js} +506 -791
- package/dist/lime-elements/lime-elements.esm.js +1 -1
- package/dist/lime-elements/{p-05afb316.entry.js → p-20658306.entry.js} +1 -1
- package/dist/lime-elements/p-BPzhuHcl.js +1 -0
- package/dist/lime-elements/p-CC4Usa-Q.js +7 -0
- package/dist/lime-elements/p-_UXAljNc.js +1 -0
- package/dist/lime-elements/p-dc9cd793.entry.js +1 -0
- package/dist/lime-elements/{p-10594468.entry.js → p-dd62042a.entry.js} +1 -1
- package/dist/types/components/markdown/markdown-exporter.d.ts +15 -0
- package/dist/types/components/markdown/markdown.d.ts +19 -0
- package/dist/types/components.d.ts +10 -0
- package/dist/types/global/shared-types/custom-element.types.d.ts +25 -0
- package/package.json +7 -1
- package/dist/lime-elements/p-6d5b23ad.entry.js +0 -1
- package/dist/lime-elements/p-C9xHOCE_.js +0 -7
- package/dist/lime-elements/p-DMNtoJ8I.js +0 -1
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { fromDom } from "hast-util-from-dom";
|
|
2
|
+
import { defaultHandlers } from "hast-util-to-mdast";
|
|
3
|
+
import { defaultHandlers as defaultMarkdownHandlers, } from "mdast-util-to-markdown";
|
|
4
|
+
import rehypeRemark from "rehype-remark";
|
|
5
|
+
import remarkGfm from "remark-gfm";
|
|
6
|
+
import remarkParse from "remark-parse";
|
|
7
|
+
import remarkStringify from "remark-stringify";
|
|
8
|
+
import { unified } from "unified";
|
|
9
|
+
/**
|
|
10
|
+
* Convert rendered markdown content back to markdown, replacing each
|
|
11
|
+
* whitelisted custom element with the markdown it describes itself as.
|
|
12
|
+
*
|
|
13
|
+
* Elements that do not implement {@link MarkdownDescribable} are
|
|
14
|
+
* converted like any unknown element: their light DOM children are kept,
|
|
15
|
+
* and an element without children disappears.
|
|
16
|
+
*
|
|
17
|
+
* @param container - The element holding the rendered content.
|
|
18
|
+
* @param whitelist - The custom elements allowed in the content.
|
|
19
|
+
* @returns The content as GitHub Flavored Markdown.
|
|
20
|
+
*/
|
|
21
|
+
export async function exportMarkdown(container, whitelist) {
|
|
22
|
+
const tagNames = new Set(whitelist
|
|
23
|
+
.map((definition) => definition.tagName)
|
|
24
|
+
.filter(isDescribableTag));
|
|
25
|
+
// The tree is captured before any description is awaited, so that
|
|
26
|
+
// content rendered in the meantime cannot end up in the export without
|
|
27
|
+
// its description.
|
|
28
|
+
const describableElements = new Map();
|
|
29
|
+
const tree = fromDom(container, {
|
|
30
|
+
afterTransform: (domNode, hastNode) => {
|
|
31
|
+
if (hastNode.type !== 'element') {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
restoreDeferredImage(hastNode);
|
|
35
|
+
hideFootnoteBackReference(hastNode);
|
|
36
|
+
if (domNode instanceof Element && tagNames.has(domNode.localName)) {
|
|
37
|
+
describableElements.set(hastNode, domNode);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
await describeElements(describableElements);
|
|
42
|
+
const handlers = {
|
|
43
|
+
sup: handleSup,
|
|
44
|
+
section: handleSection,
|
|
45
|
+
};
|
|
46
|
+
for (const tagName of tagNames) {
|
|
47
|
+
handlers[tagName] = handleCustomElement;
|
|
48
|
+
}
|
|
49
|
+
const processor = unified()
|
|
50
|
+
.use(rehypeRemark, { handlers })
|
|
51
|
+
.use(remarkGfm)
|
|
52
|
+
.use(remarkStringify, {
|
|
53
|
+
bullet: '-',
|
|
54
|
+
handlers: { break: handleBreak },
|
|
55
|
+
});
|
|
56
|
+
const mdast = await processor.run({
|
|
57
|
+
type: 'root',
|
|
58
|
+
children: tree.children,
|
|
59
|
+
});
|
|
60
|
+
return processor.stringify(mdast).trimEnd();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The description is kept on the node itself, since `rehype-remark`
|
|
64
|
+
* works on a clone of the tree. It also becomes the node's only child, so
|
|
65
|
+
* that whitespace minification treats the element as content and keeps
|
|
66
|
+
* the spaces around it. An element whose description fails is converted
|
|
67
|
+
* as if it did not describe itself.
|
|
68
|
+
* @param elements
|
|
69
|
+
*/
|
|
70
|
+
async function describeElements(elements) {
|
|
71
|
+
await Promise.all([...elements].map(async ([node, element]) => {
|
|
72
|
+
try {
|
|
73
|
+
const description = await element.toMarkdown();
|
|
74
|
+
node.properties.dataMarkdownDescription = description;
|
|
75
|
+
node.children = [{ type: 'text', value: description }];
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error('Failed to describe element as markdown', error);
|
|
79
|
+
}
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
const handleCustomElement = (state, node) => {
|
|
83
|
+
const description = node.properties.dataMarkdownDescription;
|
|
84
|
+
if (typeof description === 'string') {
|
|
85
|
+
return parseMarkdown(description);
|
|
86
|
+
}
|
|
87
|
+
return state.all(node);
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Write hard line breaks as two trailing spaces rather than a backslash,
|
|
91
|
+
* since the export is also read as plain text, where the spaces are
|
|
92
|
+
* invisible.
|
|
93
|
+
* @param args
|
|
94
|
+
*/
|
|
95
|
+
const handleBreak = (...args) => {
|
|
96
|
+
const markdown = defaultMarkdownHandlers.break(...args);
|
|
97
|
+
return markdown === '\\\n' ? ' \n' : markdown;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Parse a description into mdast nodes. A description that is a single
|
|
101
|
+
* paragraph gives phrasing content, so that an element inside a
|
|
102
|
+
* paragraph or list item stays inline.
|
|
103
|
+
* @param markdown
|
|
104
|
+
*/
|
|
105
|
+
function parseMarkdown(markdown) {
|
|
106
|
+
const { children } = unified()
|
|
107
|
+
.use(remarkParse)
|
|
108
|
+
.use(remarkGfm)
|
|
109
|
+
.parse(markdown);
|
|
110
|
+
if (children.length === 1 && children[0].type === 'paragraph') {
|
|
111
|
+
return children[0].children;
|
|
112
|
+
}
|
|
113
|
+
return children;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* An image that has not been scrolled into view yet keeps its URL in
|
|
117
|
+
* `data-src`, until the lazy loading moves it to `src`.
|
|
118
|
+
* @param node
|
|
119
|
+
*/
|
|
120
|
+
function restoreDeferredImage(node) {
|
|
121
|
+
const deferredSrc = node.properties.dataSrc;
|
|
122
|
+
if (node.tagName === 'img' && !node.properties.src && deferredSrc) {
|
|
123
|
+
node.properties.src = deferredSrc;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The links from a footnote back to its references are recreated when the
|
|
128
|
+
* markdown is rendered again.
|
|
129
|
+
* @param node
|
|
130
|
+
*/
|
|
131
|
+
function hideFootnoteBackReference(node) {
|
|
132
|
+
if (node.tagName === 'a' &&
|
|
133
|
+
node.properties.dataFootnoteBackref !== undefined) {
|
|
134
|
+
node.properties.dataMdast = 'ignore';
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Footnotes are labelled with their rendered numbers. The ids only keep an
|
|
139
|
+
* encoded, lowercased form of the original labels, which cannot be turned
|
|
140
|
+
* back into them reliably. A reference shows its footnote's number, and
|
|
141
|
+
* the definitions are listed in the same order.
|
|
142
|
+
* @param state
|
|
143
|
+
* @param node
|
|
144
|
+
*/
|
|
145
|
+
const handleSup = (state, node) => {
|
|
146
|
+
const link = node.children.find((child) => isElement(child, 'a') &&
|
|
147
|
+
child.properties.dataFootnoteRef !== undefined);
|
|
148
|
+
if (!link) {
|
|
149
|
+
return defaultHandlers.sup(state, node);
|
|
150
|
+
}
|
|
151
|
+
const identifier = link.children
|
|
152
|
+
.map((child) => (child.type === 'text' ? child.value : ''))
|
|
153
|
+
.join('');
|
|
154
|
+
return { type: 'footnoteReference', identifier, label: identifier };
|
|
155
|
+
};
|
|
156
|
+
const handleSection = (state, node) => {
|
|
157
|
+
if (node.properties.dataFootnotes === undefined) {
|
|
158
|
+
return defaultHandlers.section(state, node);
|
|
159
|
+
}
|
|
160
|
+
const list = node.children.find((child) => isElement(child, 'ol'));
|
|
161
|
+
const items = list === null || list === void 0 ? void 0 : list.children.filter((child) => isElement(child, 'li'));
|
|
162
|
+
return (items !== null && items !== void 0 ? items : []).map((item, index) => {
|
|
163
|
+
const identifier = String(index + 1);
|
|
164
|
+
return {
|
|
165
|
+
type: 'footnoteDefinition',
|
|
166
|
+
identifier,
|
|
167
|
+
label: identifier,
|
|
168
|
+
children: state.toFlow(state.all(item)),
|
|
169
|
+
};
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
function isElement(node, tagName) {
|
|
173
|
+
return node.type === 'element' && node.tagName === tagName;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Whether the custom element registered for the tag implements
|
|
177
|
+
* {@link MarkdownDescribable}. Stencil defines a component's methods on
|
|
178
|
+
* the element's prototype when the tag is registered, before the
|
|
179
|
+
* component itself has loaded.
|
|
180
|
+
* @param tagName
|
|
181
|
+
*/
|
|
182
|
+
function isDescribableTag(tagName) {
|
|
183
|
+
var _a;
|
|
184
|
+
const prototype = (_a = customElements.get(tagName)) === null || _a === void 0 ? void 0 : _a.prototype;
|
|
185
|
+
return typeof (prototype === null || prototype === void 0 ? void 0 : prototype.toMarkdown) === 'function';
|
|
186
|
+
}
|
|
@@ -2,6 +2,7 @@ import { unified } from "unified";
|
|
|
2
2
|
import remarkParse from "remark-parse";
|
|
3
3
|
import remarkRehype from "remark-rehype";
|
|
4
4
|
import remarkGfm from "remark-gfm";
|
|
5
|
+
import remarkBreaks from "remark-breaks";
|
|
5
6
|
import rehypeParse from "rehype-parse";
|
|
6
7
|
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
|
|
7
8
|
import rehypeStringify from "rehype-stringify";
|
|
@@ -27,12 +28,10 @@ import { createRemoveEmptyParagraphsPlugin } from "./remove-empty-paragraphs-plu
|
|
|
27
28
|
*/
|
|
28
29
|
export async function markdownToHTML(text, options) {
|
|
29
30
|
var _a;
|
|
30
|
-
if (options === null || options === void 0 ? void 0 : options.forceHardLineBreaks) {
|
|
31
|
-
text = text.replaceAll(/(?<!\\)([\n\r])/g, ' $1');
|
|
32
|
-
}
|
|
33
31
|
const file = await unified()
|
|
34
32
|
.use(remarkParse)
|
|
35
33
|
.use(remarkGfm)
|
|
34
|
+
.use((options === null || options === void 0 ? void 0 : options.forceHardLineBreaks) ? [remarkBreaks] : [])
|
|
36
35
|
.use(remarkRehype, { allowDangerousHtml: true })
|
|
37
36
|
.use(rehypeRaw)
|
|
38
37
|
.use(createLinksPlugin())
|
|
@@ -39,6 +39,7 @@ import { adaptColorContrast } from "../../util/adapt-color-contrast";
|
|
|
39
39
|
* @exampleComponent limel-example-markdown-custom-component-with-json-props
|
|
40
40
|
* @exampleComponent limel-example-markdown-remove-empty-paragraphs
|
|
41
41
|
* @exampleComponent limel-example-markdown-adapt-color-contrast
|
|
42
|
+
* @exampleComponent limel-example-markdown-to-markdown
|
|
42
43
|
* @exampleComponent limel-example-markdown-composite
|
|
43
44
|
*/
|
|
44
45
|
export class Markdown {
|
|
@@ -97,7 +98,31 @@ export class Markdown {
|
|
|
97
98
|
this.adaptColorContrast = false;
|
|
98
99
|
this.imageIntersectionObserver = null;
|
|
99
100
|
}
|
|
100
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Returns the content as markdown, with every whitelisted custom
|
|
103
|
+
* element replaced by the markdown it describes itself as.
|
|
104
|
+
*
|
|
105
|
+
* Use this to hand the content to a target that cannot render custom
|
|
106
|
+
* elements, such as the clipboard. An element describes itself by
|
|
107
|
+
* implementing `MarkdownDescribable`; elements that do not keep
|
|
108
|
+
* their child content, or are removed if they have none.
|
|
109
|
+
*
|
|
110
|
+
* The markdown is generated from the rendered content, so it may be
|
|
111
|
+
* formatted differently from `value`.
|
|
112
|
+
*
|
|
113
|
+
* @alpha
|
|
114
|
+
* @returns The content as markdown.
|
|
115
|
+
*/
|
|
116
|
+
async toMarkdown() {
|
|
117
|
+
await this.rendering;
|
|
118
|
+
const { exportMarkdown } = await import('./markdown-exporter');
|
|
119
|
+
return exportMarkdown(this.rootElement, this.cachedCombinedWhitelist);
|
|
120
|
+
}
|
|
121
|
+
textChanged() {
|
|
122
|
+
this.rendering = this.renderMarkdown();
|
|
123
|
+
return this.rendering;
|
|
124
|
+
}
|
|
125
|
+
async renderMarkdown() {
|
|
101
126
|
try {
|
|
102
127
|
this.cleanupImageIntersectionObserver();
|
|
103
128
|
// The whitelist merge and default import live here (not in
|
|
@@ -145,7 +170,7 @@ export class Markdown {
|
|
|
145
170
|
this.cleanupImageIntersectionObserver();
|
|
146
171
|
}
|
|
147
172
|
render() {
|
|
148
|
-
return (h(Host, { key: '
|
|
173
|
+
return (h(Host, { key: '86a21b8636af14891a657e136a9dceae4acb3b23' }, h("div", { key: '5f53c998000e2f197953232a63b2deb8ee1878bf', id: "markdown", ref: (el) => (this.rootElement = el) })));
|
|
149
174
|
}
|
|
150
175
|
setupImageIntersectionObserver() {
|
|
151
176
|
if (this.lazyLoadImages) {
|
|
@@ -282,6 +307,33 @@ export class Markdown {
|
|
|
282
307
|
}
|
|
283
308
|
};
|
|
284
309
|
}
|
|
310
|
+
static get methods() {
|
|
311
|
+
return {
|
|
312
|
+
"toMarkdown": {
|
|
313
|
+
"complexType": {
|
|
314
|
+
"signature": "() => Promise<string>",
|
|
315
|
+
"parameters": [],
|
|
316
|
+
"references": {
|
|
317
|
+
"Promise": {
|
|
318
|
+
"location": "global",
|
|
319
|
+
"id": "global::Promise"
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
"return": "Promise<string>"
|
|
323
|
+
},
|
|
324
|
+
"docs": {
|
|
325
|
+
"text": "Returns the content as markdown, with every whitelisted custom\nelement replaced by the markdown it describes itself as.\n\nUse this to hand the content to a target that cannot render custom\nelements, such as the clipboard. An element describes itself by\nimplementing `MarkdownDescribable`; elements that do not keep\ntheir child content, or are removed if they have none.\n\nThe markdown is generated from the rendered content, so it may be\nformatted differently from `value`.",
|
|
326
|
+
"tags": [{
|
|
327
|
+
"name": "alpha",
|
|
328
|
+
"text": undefined
|
|
329
|
+
}, {
|
|
330
|
+
"name": "returns",
|
|
331
|
+
"text": "The content as markdown."
|
|
332
|
+
}]
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
285
337
|
static get watchers() {
|
|
286
338
|
return [{
|
|
287
339
|
"propName": "value",
|