@teleporthq/teleport-shared 0.43.71 → 0.43.75
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/__tests__/_fixtures/rich-text/README.md +6 -0
- package/__tests__/_fixtures/rich-text/help-accept-an-invitation.html +50 -0
- package/__tests__/_fixtures/rich-text/help-activate-an-appsumo-license.html +44 -0
- package/__tests__/_fixtures/rich-text/help-add-custom-code-to-your-site.html +61 -0
- package/__tests__/_fixtures/rich-text/help-connect-a-custom-domain.html +106 -0
- package/__tests__/_fixtures/rich-text/help-custom-animations-with-code.html +69 -0
- package/__tests__/_fixtures/rich-text/help-generate-a-store.html +78 -0
- package/__tests__/_fixtures/rich-text/help-keyboard-shortcuts.html +417 -0
- package/__tests__/_fixtures/rich-text/help-prompting-tips.html +82 -0
- package/__tests__/_fixtures/rich-text/help-sitemap-robots-and-llms-txt.html +82 -0
- package/__tests__/_fixtures/rich-text/help-use-a-csv-file-or-static-data.html +61 -0
- package/__tests__/_helpers/rich-text-content-runtime-emitter.ts +117 -0
- package/__tests__/function-sources.ts +30 -0
- package/__tests__/utils/content-tables.ts +69 -0
- package/__tests__/utils/rich-text-content-runtime-module.ts +115 -0
- package/__tests__/utils/rich-text-content.ts +131 -0
- package/dist/cjs/index.d.ts +4 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +7 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/page-transition/index.d.ts +1 -0
- package/dist/cjs/page-transition/index.d.ts.map +1 -1
- package/dist/cjs/page-transition/index.js +1 -0
- package/dist/cjs/page-transition/index.js.map +1 -1
- package/dist/cjs/page-transition/variant-sources.generated.d.ts +7 -0
- package/dist/cjs/page-transition/variant-sources.generated.d.ts.map +1 -0
- package/dist/cjs/page-transition/variant-sources.generated.js +14 -0
- package/dist/cjs/page-transition/variant-sources.generated.js.map +1 -0
- package/dist/cjs/scroll-scene/engine-source.d.ts.map +1 -1
- package/dist/cjs/scroll-scene/engine-source.js +2 -5
- package/dist/cjs/scroll-scene/engine-source.js.map +1 -1
- package/dist/cjs/scroll-scene/helper-sources.generated.d.ts +9 -0
- package/dist/cjs/scroll-scene/helper-sources.generated.d.ts.map +1 -0
- package/dist/cjs/scroll-scene/helper-sources.generated.js +16 -0
- package/dist/cjs/scroll-scene/helper-sources.generated.js.map +1 -0
- package/dist/cjs/scroll-scene/index.d.ts +1 -0
- package/dist/cjs/scroll-scene/index.d.ts.map +1 -1
- package/dist/cjs/scroll-scene/index.js +3 -1
- package/dist/cjs/scroll-scene/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils/content-tables.d.ts +78 -0
- package/dist/cjs/utils/content-tables.d.ts.map +1 -0
- package/dist/cjs/utils/content-tables.js +123 -0
- package/dist/cjs/utils/content-tables.js.map +1 -0
- package/dist/cjs/utils/rich-text-content-codegen.d.ts +20 -0
- package/dist/cjs/utils/rich-text-content-codegen.d.ts.map +1 -0
- package/dist/cjs/utils/rich-text-content-codegen.js +55 -0
- package/dist/cjs/utils/rich-text-content-codegen.js.map +1 -0
- package/dist/cjs/utils/rich-text-content-runtime-source.d.ts +2 -0
- package/dist/cjs/utils/rich-text-content-runtime-source.d.ts.map +1 -0
- package/dist/cjs/utils/rich-text-content-runtime-source.js +591 -0
- package/dist/cjs/utils/rich-text-content-runtime-source.js.map +1 -0
- package/dist/cjs/utils/rich-text-content.d.ts +150 -0
- package/dist/cjs/utils/rich-text-content.d.ts.map +1 -0
- package/dist/cjs/utils/rich-text-content.js +649 -0
- package/dist/cjs/utils/rich-text-content.js.map +1 -0
- package/dist/cjs/utils/table-access.d.ts.map +1 -1
- package/dist/cjs/utils/table-access.js +5 -7
- package/dist/cjs/utils/table-access.js.map +1 -1
- package/dist/esm/index.d.ts +4 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +4 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/page-transition/index.d.ts +1 -0
- package/dist/esm/page-transition/index.d.ts.map +1 -1
- package/dist/esm/page-transition/index.js +1 -0
- package/dist/esm/page-transition/index.js.map +1 -1
- package/dist/esm/page-transition/variant-sources.generated.d.ts +7 -0
- package/dist/esm/page-transition/variant-sources.generated.d.ts.map +1 -0
- package/dist/esm/page-transition/variant-sources.generated.js +11 -0
- package/dist/esm/page-transition/variant-sources.generated.js.map +1 -0
- package/dist/esm/scroll-scene/engine-source.d.ts.map +1 -1
- package/dist/esm/scroll-scene/engine-source.js +2 -5
- package/dist/esm/scroll-scene/engine-source.js.map +1 -1
- package/dist/esm/scroll-scene/helper-sources.generated.d.ts +9 -0
- package/dist/esm/scroll-scene/helper-sources.generated.d.ts.map +1 -0
- package/dist/esm/scroll-scene/helper-sources.generated.js +13 -0
- package/dist/esm/scroll-scene/helper-sources.generated.js.map +1 -0
- package/dist/esm/scroll-scene/index.d.ts +1 -0
- package/dist/esm/scroll-scene/index.d.ts.map +1 -1
- package/dist/esm/scroll-scene/index.js +1 -0
- package/dist/esm/scroll-scene/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils/content-tables.d.ts +78 -0
- package/dist/esm/utils/content-tables.d.ts.map +1 -0
- package/dist/esm/utils/content-tables.js +114 -0
- package/dist/esm/utils/content-tables.js.map +1 -0
- package/dist/esm/utils/rich-text-content-codegen.d.ts +20 -0
- package/dist/esm/utils/rich-text-content-codegen.d.ts.map +1 -0
- package/dist/esm/utils/rich-text-content-codegen.js +50 -0
- package/dist/esm/utils/rich-text-content-codegen.js.map +1 -0
- package/dist/esm/utils/rich-text-content-runtime-source.d.ts +2 -0
- package/dist/esm/utils/rich-text-content-runtime-source.d.ts.map +1 -0
- package/dist/esm/utils/rich-text-content-runtime-source.js +588 -0
- package/dist/esm/utils/rich-text-content-runtime-source.js.map +1 -0
- package/dist/esm/utils/rich-text-content.d.ts +150 -0
- package/dist/esm/utils/rich-text-content.d.ts.map +1 -0
- package/dist/esm/utils/rich-text-content.js +613 -0
- package/dist/esm/utils/rich-text-content.js.map +1 -0
- package/dist/esm/utils/table-access.d.ts.map +1 -1
- package/dist/esm/utils/table-access.js +5 -7
- package/dist/esm/utils/table-access.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +6 -0
- package/src/page-transition/index.ts +1 -0
- package/src/page-transition/variant-sources.generated.ts +14 -0
- package/src/scroll-scene/engine-source.ts +7 -10
- package/src/scroll-scene/helper-sources.generated.ts +18 -0
- package/src/scroll-scene/index.ts +1 -0
- package/src/utils/content-tables.ts +151 -0
- package/src/utils/rich-text-content-codegen.ts +52 -0
- package/src/utils/rich-text-content-runtime-source.ts +587 -0
- package/src/utils/rich-text-content.ts +677 -0
- package/src/utils/table-access.ts +4 -2
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stored form of rich-text content (a blog post body, a product
|
|
3
|
+
* description) and its editor form.
|
|
4
|
+
*
|
|
5
|
+
* STORED: plain HTML — `<p>`, `<h1>`–`<h6>`, nested `<ul>`/`<ol>`, `<blockquote>`
|
|
6
|
+
* holding paragraphs, `<pre><code class="language-x">`, `<table>` with its
|
|
7
|
+
* `<thead>`/`<th>`, `<aside class="tq-callout" data-kind="note">`, the image and
|
|
8
|
+
* embed figures, and inline marks. What a published page renders and what an
|
|
9
|
+
* import writes.
|
|
10
|
+
*
|
|
11
|
+
* EDITOR: what a Quill editor can hold without rewriting it. Lists become one
|
|
12
|
+
* flat `<ol>` of `<li data-list>` lines with `ql-indent-N` for nesting, a
|
|
13
|
+
* blockquote becomes one `<blockquote>` line per paragraph, and everything Quill
|
|
14
|
+
* has no blot for — a table, a code block, a callout, a list whose items hold
|
|
15
|
+
* blocks, anything unknown — is wrapped in a PROTECTED block:
|
|
16
|
+
* `<div class="tq-protected" data-tq-protected="table" contenteditable="false">`
|
|
17
|
+
* holding the stored markup untouched. The editors register that wrapper as a
|
|
18
|
+
* block embed, so Quill treats it as one atomic unit and never looks inside.
|
|
19
|
+
*
|
|
20
|
+
* Both editors (the studio post form on the `quill` fork, the generated admin
|
|
21
|
+
* panel on react-quill-new) run `toEditorHtml` on the way in and
|
|
22
|
+
* `fromEditorHtml` on the way out. `normalizeLegacyRichTextHtml` additionally
|
|
23
|
+
* understands what the two editors wrote BEFORE this contract existed, so an
|
|
24
|
+
* older post is cleaned on its next save and renders right meanwhile — it is
|
|
25
|
+
* string-only, so the generated project can run it where there is no DOM.
|
|
26
|
+
*
|
|
27
|
+
* Emission rules (see `__tests__/_helpers/rich-text-content-runtime-emitter.ts`):
|
|
28
|
+
* the generated admin editor receives these very functions printed as a module,
|
|
29
|
+
* so an exported function may call only other exported functions and the
|
|
30
|
+
* constants below — never an import or a module-private helper.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export const PROTECTED_BLOCK_CLASS = 'tq-protected'
|
|
34
|
+
export const PROTECTED_BLOCK_ATTR = 'data-tq-protected'
|
|
35
|
+
export const PROTECTED_BLOCK_BLOT_NAME = 'tq-protected-block'
|
|
36
|
+
|
|
37
|
+
export const PROTECTED_BLOCK_KINDS = ['table', 'code', 'callout', 'list', 'quote', 'html']
|
|
38
|
+
export type ProtectedBlockKind = 'table' | 'code' | 'callout' | 'list' | 'quote' | 'html'
|
|
39
|
+
|
|
40
|
+
export interface ProtectedBlockValue {
|
|
41
|
+
kind: ProtectedBlockKind
|
|
42
|
+
/** The stored markup of the block, exactly as it will be written back. */
|
|
43
|
+
html: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const CALLOUT_CLASS = 'tq-callout'
|
|
47
|
+
export const CALLOUT_KIND_ATTR = 'data-kind'
|
|
48
|
+
export const CALLOUT_KINDS = ['note', 'tip', 'important', 'warning', 'caution']
|
|
49
|
+
export type CalloutKind = 'note' | 'tip' | 'important' | 'warning' | 'caution'
|
|
50
|
+
|
|
51
|
+
/** ⛔ PAIRED with `EMBED_CLASS_NAME` in `rich-text-embeds.ts` (a test keeps them equal). */
|
|
52
|
+
export const RICH_TEXT_EMBED_CLASS = 'tq-embed'
|
|
53
|
+
/** ⛔ PAIRED with the image blot in teleport-gui `features/blog/components/content-image-blot.ts`. */
|
|
54
|
+
export const RICH_TEXT_IMAGE_CLASS = 'ql-content-image'
|
|
55
|
+
|
|
56
|
+
export const QUILL_LIST_ITEM_ATTR = 'data-list'
|
|
57
|
+
export const QUILL_INDENT_CLASS_PREFIX = 'ql-indent-'
|
|
58
|
+
|
|
59
|
+
/** Marks that can sit inside a line; anything else is a block to Quill. */
|
|
60
|
+
export const RICH_TEXT_INLINE_TAGS = [
|
|
61
|
+
'A',
|
|
62
|
+
'ABBR',
|
|
63
|
+
'B',
|
|
64
|
+
'BR',
|
|
65
|
+
'CODE',
|
|
66
|
+
'DEL',
|
|
67
|
+
'EM',
|
|
68
|
+
'I',
|
|
69
|
+
'IMG',
|
|
70
|
+
'INS',
|
|
71
|
+
'KBD',
|
|
72
|
+
'MARK',
|
|
73
|
+
'S',
|
|
74
|
+
'SMALL',
|
|
75
|
+
'SPAN',
|
|
76
|
+
'STRIKE',
|
|
77
|
+
'STRONG',
|
|
78
|
+
'SUB',
|
|
79
|
+
'SUP',
|
|
80
|
+
'U',
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
/** Elements that stand on their own line; white space between two of them carries nothing. */
|
|
84
|
+
export const RICH_TEXT_BLOCK_TAGS = [
|
|
85
|
+
'ARTICLE',
|
|
86
|
+
'ASIDE',
|
|
87
|
+
'BLOCKQUOTE',
|
|
88
|
+
'DD',
|
|
89
|
+
'DETAILS',
|
|
90
|
+
'DIV',
|
|
91
|
+
'DL',
|
|
92
|
+
'DT',
|
|
93
|
+
'FIGCAPTION',
|
|
94
|
+
'FIGURE',
|
|
95
|
+
'FOOTER',
|
|
96
|
+
'FORM',
|
|
97
|
+
'H1',
|
|
98
|
+
'H2',
|
|
99
|
+
'H3',
|
|
100
|
+
'H4',
|
|
101
|
+
'H5',
|
|
102
|
+
'H6',
|
|
103
|
+
'HEADER',
|
|
104
|
+
'HR',
|
|
105
|
+
'LI',
|
|
106
|
+
'MAIN',
|
|
107
|
+
'NAV',
|
|
108
|
+
'OL',
|
|
109
|
+
'P',
|
|
110
|
+
'PRE',
|
|
111
|
+
'SECTION',
|
|
112
|
+
'SUMMARY',
|
|
113
|
+
'TABLE',
|
|
114
|
+
'TBODY',
|
|
115
|
+
'TD',
|
|
116
|
+
'TFOOT',
|
|
117
|
+
'TH',
|
|
118
|
+
'THEAD',
|
|
119
|
+
'TR',
|
|
120
|
+
'UL',
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
/** Parses an HTML fragment into a detached body the way a browser would. */
|
|
124
|
+
export function parseRichTextFragment(html: string): HTMLElement {
|
|
125
|
+
const doc = new DOMParser().parseFromString('<!doctype html><html><body>' + html, 'text/html')
|
|
126
|
+
return doc.body
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function escapeRichTextText(value: string): string {
|
|
130
|
+
return String(value).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function escapeRichTextAttribute(value: string): string {
|
|
134
|
+
return escapeRichTextText(value).replace(/"/g, '"')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function isProtectedBlockKind(
|
|
138
|
+
value: string | null | undefined
|
|
139
|
+
): value is ProtectedBlockKind {
|
|
140
|
+
return PROTECTED_BLOCK_KINDS.indexOf(String(value)) !== -1
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The editor-side wrapper for one stored block. */
|
|
144
|
+
export function buildProtectedBlockHtml(kind: ProtectedBlockKind, html: string): string {
|
|
145
|
+
return (
|
|
146
|
+
`<div class="${PROTECTED_BLOCK_CLASS}" ${PROTECTED_BLOCK_ATTR}="${kind}" contenteditable="false">` +
|
|
147
|
+
html +
|
|
148
|
+
'</div>'
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function readProtectedBlockValue(element: Element): ProtectedBlockValue {
|
|
153
|
+
const kind = element.getAttribute(PROTECTED_BLOCK_ATTR)
|
|
154
|
+
return {
|
|
155
|
+
kind: isProtectedBlockKind(kind) ? kind : 'html',
|
|
156
|
+
html: element.innerHTML,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function isCalloutKind(value: string | null | undefined): value is CalloutKind {
|
|
161
|
+
return CALLOUT_KINDS.indexOf(String(value)) !== -1
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** The stored markup of a callout: a kind and the blocks it holds. */
|
|
165
|
+
export function buildCalloutHtml(kind: CalloutKind, innerHtml: string): string {
|
|
166
|
+
return `<aside class="${CALLOUT_CLASS}" ${CALLOUT_KIND_ATTR}="${kind}">${innerHtml}</aside>`
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The stored markup of a code block; `code` is already HTML-escaped when `escaped` is set. */
|
|
170
|
+
export function buildCodeBlockHtml(language: string, code: string, escaped?: boolean): string {
|
|
171
|
+
const text = escaped ? code : escapeRichTextText(code || '')
|
|
172
|
+
const lang = String(language || '').replace(/[^\w.+#-]/g, '')
|
|
173
|
+
const classAttr = lang && lang !== 'plain' ? ` class="language-${lang}"` : ''
|
|
174
|
+
return `<pre><code${classAttr}>${text}</code></pre>`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function hasClass(element: Element, className: string): boolean {
|
|
178
|
+
const classes = (element.getAttribute('class') || '').split(/\s+/)
|
|
179
|
+
return classes.indexOf(className) !== -1
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function isRichTextInlineNode(node: Node): boolean {
|
|
183
|
+
if (node.nodeType === 3 || node.nodeType === 8) {
|
|
184
|
+
return true
|
|
185
|
+
}
|
|
186
|
+
if (node.nodeType !== 1) {
|
|
187
|
+
return false
|
|
188
|
+
}
|
|
189
|
+
return RICH_TEXT_INLINE_TAGS.indexOf((node as Element).tagName) !== -1
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function isBlankTextNode(node: Node): boolean {
|
|
193
|
+
return node.nodeType === 3 && !/\S/.test(node.nodeValue || '')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function isRichTextBlockElement(node: Node | null): boolean {
|
|
197
|
+
return (
|
|
198
|
+
!!node && node.nodeType === 1 && RICH_TEXT_BLOCK_TAGS.indexOf((node as Element).tagName) !== -1
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Drops the white space that only separates blocks (the newlines a Markdown
|
|
204
|
+
* renderer puts between `<li>`s, after `</p>`) and collapses runs of it inside
|
|
205
|
+
* text to the one space they render as, so stored HTML has one spelling
|
|
206
|
+
* whatever wrote it. Everything inside `<pre>` stays.
|
|
207
|
+
*/
|
|
208
|
+
export function stripInterBlockWhitespace(root: Node): void {
|
|
209
|
+
if (root.nodeType === 1 && (root as Element).tagName === 'PRE') {
|
|
210
|
+
return
|
|
211
|
+
}
|
|
212
|
+
const rootIsBlock = root.nodeType !== 1 || isRichTextBlockElement(root)
|
|
213
|
+
const children = Array.prototype.slice.call(root.childNodes) as Node[]
|
|
214
|
+
children.forEach((child) => {
|
|
215
|
+
if (child.nodeType === 3) {
|
|
216
|
+
const afterBlock =
|
|
217
|
+
isRichTextBlockElement(child.previousSibling) || (rootIsBlock && !child.previousSibling)
|
|
218
|
+
const beforeBlock =
|
|
219
|
+
isRichTextBlockElement(child.nextSibling) || (rootIsBlock && !child.nextSibling)
|
|
220
|
+
// A run of white space renders as one space; stored HTML keeps it that way
|
|
221
|
+
// (a no-break space is not white space here).
|
|
222
|
+
let text = (child.nodeValue || '').replace(/[ \t\r\n\f]+/g, ' ')
|
|
223
|
+
if (afterBlock) {
|
|
224
|
+
text = text.replace(/^\s+/, '')
|
|
225
|
+
}
|
|
226
|
+
if (beforeBlock) {
|
|
227
|
+
text = text.replace(/\s+$/, '')
|
|
228
|
+
}
|
|
229
|
+
if (!text && (afterBlock || beforeBlock)) {
|
|
230
|
+
root.removeChild(child)
|
|
231
|
+
} else if (text !== child.nodeValue) {
|
|
232
|
+
child.nodeValue = text
|
|
233
|
+
}
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
if (child.nodeType === 1) {
|
|
237
|
+
stripInterBlockWhitespace(child)
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** `<li><p>one paragraph</p></li>` means the same as `<li>one paragraph</li>`; stored HTML uses the short form. */
|
|
243
|
+
export function unwrapSingleParagraphListItems(root: Element): void {
|
|
244
|
+
Array.prototype.slice.call(root.querySelectorAll('li')).forEach((item: Element) => {
|
|
245
|
+
const paragraphs = Array.prototype.slice
|
|
246
|
+
.call(item.children)
|
|
247
|
+
.filter((child: Element) => child.tagName === 'P') as Element[]
|
|
248
|
+
const blocks = Array.prototype.slice
|
|
249
|
+
.call(item.children)
|
|
250
|
+
.filter((child: Element) => !isRichTextInlineNode(child)) as Element[]
|
|
251
|
+
const nested = blocks.filter((child) => child.tagName === 'UL' || child.tagName === 'OL')
|
|
252
|
+
if (paragraphs.length !== 1 || blocks.length !== 1 + nested.length) {
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
const paragraph = paragraphs[0]
|
|
256
|
+
while (paragraph.firstChild) {
|
|
257
|
+
item.insertBefore(paragraph.firstChild, paragraph)
|
|
258
|
+
}
|
|
259
|
+
item.removeChild(paragraph)
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** A link to another web page: an absolute http(s) address. Everything else stays in the tab. */
|
|
264
|
+
export function isExternalWebLink(href: string): boolean {
|
|
265
|
+
return /^https?:\/\//i.test(String(href || '').trim())
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Quill creates every link with `target="_blank"`. Stored HTML decides for
|
|
270
|
+
* itself: a link to another web page opens in a new tab, a link to a page of
|
|
271
|
+
* the same site (or a mail or phone link) opens in this one.
|
|
272
|
+
*/
|
|
273
|
+
export function normalizeLinkTargets(root: Element): void {
|
|
274
|
+
Array.prototype.slice.call(root.querySelectorAll('a[href]')).forEach((link: Element) => {
|
|
275
|
+
if (isExternalWebLink(link.getAttribute('href') || '')) {
|
|
276
|
+
if (!link.getAttribute('target')) {
|
|
277
|
+
link.setAttribute('rel', 'noopener noreferrer')
|
|
278
|
+
link.setAttribute('target', '_blank')
|
|
279
|
+
}
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
link.removeAttribute('target')
|
|
283
|
+
link.removeAttribute('rel')
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** A quote holds paragraphs; bare inline content inside one becomes its single paragraph. */
|
|
288
|
+
export function wrapBareBlockquoteContent(root: Element): void {
|
|
289
|
+
Array.prototype.slice.call(root.querySelectorAll('blockquote')).forEach((quote: Element) => {
|
|
290
|
+
const children = Array.prototype.slice.call(quote.childNodes) as Node[]
|
|
291
|
+
const bare = children.filter((child) => !isBlankTextNode(child))
|
|
292
|
+
if (!bare.length || !bare.every((child) => isRichTextInlineNode(child))) {
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
const paragraph = quote.ownerDocument.createElement('p')
|
|
296
|
+
children.forEach((child) => paragraph.appendChild(child))
|
|
297
|
+
quote.appendChild(paragraph)
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** The markup of one node as it sits in its parent: text escaped, elements whole. */
|
|
302
|
+
export function serializeRichTextNode(node: Node): string {
|
|
303
|
+
if (node.nodeType === 1) {
|
|
304
|
+
return (node as Element).outerHTML
|
|
305
|
+
}
|
|
306
|
+
if (node.nodeType === 3) {
|
|
307
|
+
return escapeRichTextText(node.nodeValue || '')
|
|
308
|
+
}
|
|
309
|
+
return ''
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The inline markup of a list item: text and marks, a single wrapping `<p>`
|
|
314
|
+
* unwrapped, nested lists left out.
|
|
315
|
+
*/
|
|
316
|
+
export function readInlineHtml(element: Element): string {
|
|
317
|
+
let html = ''
|
|
318
|
+
Array.prototype.slice.call(element.childNodes).forEach((node: Node) => {
|
|
319
|
+
if (node.nodeType === 1) {
|
|
320
|
+
const child = node as Element
|
|
321
|
+
if (child.tagName === 'UL' || child.tagName === 'OL') {
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
if (child.tagName === 'P') {
|
|
325
|
+
html += child.innerHTML
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
html += serializeRichTextNode(node)
|
|
330
|
+
})
|
|
331
|
+
return html.replace(/^\s+|\s+$/g, '')
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* A list Quill can hold as lines: every item is inline content (at most one
|
|
336
|
+
* `<p>` around it) followed, at most, by one nested list that is itself simple,
|
|
337
|
+
* and no `start` offset on a numbered list.
|
|
338
|
+
*/
|
|
339
|
+
export function isQuillSimpleList(list: Element): boolean {
|
|
340
|
+
if (list.hasAttribute('start')) {
|
|
341
|
+
return false
|
|
342
|
+
}
|
|
343
|
+
const items = Array.prototype.slice.call(list.children) as Element[]
|
|
344
|
+
for (let i = 0; i < items.length; i += 1) {
|
|
345
|
+
const item = items[i]
|
|
346
|
+
if (item.tagName !== 'LI') {
|
|
347
|
+
return false
|
|
348
|
+
}
|
|
349
|
+
const children = Array.prototype.slice.call(item.childNodes) as Node[]
|
|
350
|
+
let paragraphs = 0
|
|
351
|
+
let nested: Element | null = null
|
|
352
|
+
for (let j = 0; j < children.length; j += 1) {
|
|
353
|
+
const child = children[j]
|
|
354
|
+
if (isRichTextInlineNode(child)) {
|
|
355
|
+
if (nested && !isBlankTextNode(child)) {
|
|
356
|
+
return false
|
|
357
|
+
}
|
|
358
|
+
continue
|
|
359
|
+
}
|
|
360
|
+
const tag = (child as Element).tagName
|
|
361
|
+
if (tag === 'P') {
|
|
362
|
+
paragraphs += 1
|
|
363
|
+
if (paragraphs > 1 || nested) {
|
|
364
|
+
return false
|
|
365
|
+
}
|
|
366
|
+
continue
|
|
367
|
+
}
|
|
368
|
+
if ((tag === 'UL' || tag === 'OL') && !nested) {
|
|
369
|
+
nested = child as Element
|
|
370
|
+
continue
|
|
371
|
+
}
|
|
372
|
+
return false
|
|
373
|
+
}
|
|
374
|
+
if (nested && !isQuillSimpleList(nested)) {
|
|
375
|
+
return false
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return true
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** One flat `<ol>` of `<li data-list>` lines, the shape Quill keeps a list in. */
|
|
382
|
+
export function flattenListForQuill(list: Element): string {
|
|
383
|
+
let html = '<ol>'
|
|
384
|
+
const walk = (node: Element, indent: number): void => {
|
|
385
|
+
const type = node.tagName === 'OL' ? 'ordered' : 'bullet'
|
|
386
|
+
Array.prototype.slice.call(node.children).forEach((item: Element) => {
|
|
387
|
+
if (item.tagName !== 'LI') {
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
const indentClass = indent > 0 ? ` class="${QUILL_INDENT_CLASS_PREFIX}${indent}"` : ''
|
|
391
|
+
html += `<li ${QUILL_LIST_ITEM_ATTR}="${type}"${indentClass}>${readInlineHtml(item)}</li>`
|
|
392
|
+
Array.prototype.slice.call(item.children).forEach((child: Element) => {
|
|
393
|
+
if (child.tagName === 'UL' || child.tagName === 'OL') {
|
|
394
|
+
walk(child, indent + 1)
|
|
395
|
+
}
|
|
396
|
+
})
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
walk(list, 0)
|
|
400
|
+
return html + '</ol>'
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** A blockquote Quill can hold: paragraphs (or bare inline content) only. */
|
|
404
|
+
export function isQuillSimpleBlockquote(quote: Element): boolean {
|
|
405
|
+
const children = Array.prototype.slice.call(quote.childNodes) as Node[]
|
|
406
|
+
for (let i = 0; i < children.length; i += 1) {
|
|
407
|
+
const child = children[i]
|
|
408
|
+
if (isRichTextInlineNode(child)) {
|
|
409
|
+
continue
|
|
410
|
+
}
|
|
411
|
+
if ((child as Element).tagName !== 'P') {
|
|
412
|
+
return false
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return true
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** One `<blockquote>` line per paragraph, the shape Quill keeps a quote in. */
|
|
419
|
+
export function blockquoteLinesForQuill(quote: Element): string {
|
|
420
|
+
const lines: string[] = []
|
|
421
|
+
let pending = ''
|
|
422
|
+
const flush = (): void => {
|
|
423
|
+
if (/\S/.test(pending)) {
|
|
424
|
+
lines.push(pending.replace(/^\s+|\s+$/g, ''))
|
|
425
|
+
}
|
|
426
|
+
pending = ''
|
|
427
|
+
}
|
|
428
|
+
Array.prototype.slice.call(quote.childNodes).forEach((node: Node) => {
|
|
429
|
+
if (node.nodeType === 1 && (node as Element).tagName === 'P') {
|
|
430
|
+
flush()
|
|
431
|
+
lines.push((node as Element).innerHTML)
|
|
432
|
+
return
|
|
433
|
+
}
|
|
434
|
+
pending += serializeRichTextNode(node)
|
|
435
|
+
})
|
|
436
|
+
flush()
|
|
437
|
+
return lines.map((line) => `<blockquote>${line}</blockquote>`).join('')
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Which protected kind a stored block that Quill cannot hold gets. */
|
|
441
|
+
export function protectedKindForElement(element: Element): ProtectedBlockKind {
|
|
442
|
+
const tag = element.tagName
|
|
443
|
+
if (tag === 'TABLE') {
|
|
444
|
+
return 'table'
|
|
445
|
+
}
|
|
446
|
+
if (tag === 'PRE') {
|
|
447
|
+
return 'code'
|
|
448
|
+
}
|
|
449
|
+
if (tag === 'ASIDE' && hasClass(element, CALLOUT_CLASS)) {
|
|
450
|
+
return 'callout'
|
|
451
|
+
}
|
|
452
|
+
if (tag === 'UL' || tag === 'OL') {
|
|
453
|
+
return 'list'
|
|
454
|
+
}
|
|
455
|
+
if (tag === 'BLOCKQUOTE') {
|
|
456
|
+
return 'quote'
|
|
457
|
+
}
|
|
458
|
+
return 'html'
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Stored HTML → what the editor is given. Every block Quill holds natively is
|
|
463
|
+
* handed over in Quill's own shape; every other block is protected.
|
|
464
|
+
*/
|
|
465
|
+
export function toEditorHtml(storedHtml: string): string {
|
|
466
|
+
const body = parseRichTextFragment(normalizeLegacyRichTextHtml(storedHtml))
|
|
467
|
+
let html = ''
|
|
468
|
+
Array.prototype.slice.call(body.childNodes).forEach((node: Node) => {
|
|
469
|
+
if (node.nodeType === 3) {
|
|
470
|
+
if (/\S/.test(node.nodeValue || '')) {
|
|
471
|
+
html += '<p>' + escapeRichTextText(node.nodeValue || '') + '</p>'
|
|
472
|
+
}
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
if (node.nodeType !== 1) {
|
|
476
|
+
return
|
|
477
|
+
}
|
|
478
|
+
const element = node as Element
|
|
479
|
+
const tag = element.tagName
|
|
480
|
+
if (tag === 'P' || /^H[1-6]$/.test(tag)) {
|
|
481
|
+
html += element.outerHTML
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
if (isRichTextInlineNode(element)) {
|
|
485
|
+
html += '<p>' + element.outerHTML + '</p>'
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
if (tag === 'DIV' && hasClass(element, PROTECTED_BLOCK_CLASS)) {
|
|
489
|
+
html += element.outerHTML
|
|
490
|
+
return
|
|
491
|
+
}
|
|
492
|
+
if (
|
|
493
|
+
tag === 'FIGURE' &&
|
|
494
|
+
(hasClass(element, RICH_TEXT_IMAGE_CLASS) || hasClass(element, RICH_TEXT_EMBED_CLASS))
|
|
495
|
+
) {
|
|
496
|
+
html += element.outerHTML
|
|
497
|
+
return
|
|
498
|
+
}
|
|
499
|
+
if ((tag === 'UL' || tag === 'OL') && isQuillSimpleList(element)) {
|
|
500
|
+
html += flattenListForQuill(element)
|
|
501
|
+
return
|
|
502
|
+
}
|
|
503
|
+
if (tag === 'BLOCKQUOTE' && isQuillSimpleBlockquote(element)) {
|
|
504
|
+
html += blockquoteLinesForQuill(element)
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
html += buildProtectedBlockHtml(protectedKindForElement(element), element.outerHTML)
|
|
508
|
+
})
|
|
509
|
+
return html
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* What the editor holds → stored HTML. Protected blocks are unwrapped, Quill's
|
|
514
|
+
* flat lists and blockquote lines are rebuilt, and Quill's own helpers are
|
|
515
|
+
* stripped. Idempotent on stored HTML.
|
|
516
|
+
*/
|
|
517
|
+
export function fromEditorHtml(editorHtml: string): string {
|
|
518
|
+
const body = parseRichTextFragment(editorHtml)
|
|
519
|
+
Array.prototype.slice
|
|
520
|
+
.call(body.querySelectorAll('.' + PROTECTED_BLOCK_CLASS))
|
|
521
|
+
.forEach((wrapper: Element) => {
|
|
522
|
+
const parent = wrapper.parentNode
|
|
523
|
+
if (!parent) {
|
|
524
|
+
return
|
|
525
|
+
}
|
|
526
|
+
while (wrapper.firstChild) {
|
|
527
|
+
parent.insertBefore(wrapper.firstChild, wrapper)
|
|
528
|
+
}
|
|
529
|
+
parent.removeChild(wrapper)
|
|
530
|
+
})
|
|
531
|
+
const normalized = parseRichTextFragment(normalizeLegacyRichTextHtml(body.innerHTML))
|
|
532
|
+
unwrapSingleParagraphListItems(normalized)
|
|
533
|
+
wrapBareBlockquoteContent(normalized)
|
|
534
|
+
normalizeLinkTargets(normalized)
|
|
535
|
+
stripInterBlockWhitespace(normalized)
|
|
536
|
+
return trimEmptyEdgeParagraphs(normalized.innerHTML)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** Stored HTML in its one canonical spelling, whatever dialect it arrived in. */
|
|
540
|
+
export function toCanonicalRichTextHtml(html: string): string {
|
|
541
|
+
return fromEditorHtml(html)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Quill keeps an empty line as `<p><br></p>`; one at either end carries nothing. */
|
|
545
|
+
export function trimEmptyEdgeParagraphs(html: string): string {
|
|
546
|
+
return html
|
|
547
|
+
.replace(/^(?:\s*<p>(?:<br\s*\/?>|\s| )*<\/p>)+/, '')
|
|
548
|
+
.replace(/(?:<p>(?:<br\s*\/?>|\s| )*<\/p>\s*)+$/, '')
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* What the editors wrote before this contract, turned into stored HTML. String
|
|
553
|
+
* only, so a generated page can run it in `getStaticProps`:
|
|
554
|
+
* - the studio's lists: one `<ol>` of `<li data-list="bullet|ordered">` with
|
|
555
|
+
* `ql-indent-N` and an empty `<span class="ql-ui">` per line;
|
|
556
|
+
* - the studio's code blocks: `div.ql-code-block-container` of line divs;
|
|
557
|
+
* - the studio's quotes: one `<blockquote>` element per line;
|
|
558
|
+
* - the admin panel's text: every space a no-break space;
|
|
559
|
+
* - the admin panel's code blocks: `<pre data-language>` with no `<code>`.
|
|
560
|
+
* Leaves stored HTML as it is.
|
|
561
|
+
*/
|
|
562
|
+
export function normalizeLegacyRichTextHtml(html: string): string {
|
|
563
|
+
let out = String(html || '')
|
|
564
|
+
out = out.replace(/<span class="ql-ui"[^>]*>(?:<\/span>)?/g, '')
|
|
565
|
+
out = out.replace(
|
|
566
|
+
/<(ol|ul)(\s[^>]*)?>((?:(?!<\/?(?:ol|ul)\b)[\s\S])*)<\/\1>/g,
|
|
567
|
+
(match, _tag, _attrs, inner) =>
|
|
568
|
+
inner.indexOf(QUILL_LIST_ITEM_ATTR + '=') === -1 ? match : rebuildQuillListHtml(inner)
|
|
569
|
+
)
|
|
570
|
+
out = out.replace(
|
|
571
|
+
/<div class="ql-code-block-container"[^>]*>((?:\s*<div class="ql-code-block"[^>]*>[\s\S]*?<\/div>)*)\s*<\/div>/g,
|
|
572
|
+
(_match, inner) => rebuildQuillCodeBlockHtml(inner)
|
|
573
|
+
)
|
|
574
|
+
out = out.replace(
|
|
575
|
+
/<pre data-language="([^"]*)">\n?([\s\S]*?)\n?<\/pre>/g,
|
|
576
|
+
(_match, language, code) => buildCodeBlockHtml(language, code, true)
|
|
577
|
+
)
|
|
578
|
+
out = out.replace(/((?:<blockquote>(?:(?!<\/?blockquote\b)[\s\S])*<\/blockquote>\s*)+)/g, (run) =>
|
|
579
|
+
mergeBlockquoteLines(run)
|
|
580
|
+
)
|
|
581
|
+
out = out.replace(/(\S)(?: | )(?=\S)/g, '$1 ')
|
|
582
|
+
return out
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** The nested `<ul>`/`<ol>` that a flat run of Quill list lines means. */
|
|
586
|
+
export function rebuildQuillListHtml(innerHtml: string): string {
|
|
587
|
+
const items: Array<{ type: string; indent: number; html: string }> = []
|
|
588
|
+
const pattern = /<li\b([^>]*)>([\s\S]*?)<\/li>/g
|
|
589
|
+
let match = pattern.exec(innerHtml)
|
|
590
|
+
while (match) {
|
|
591
|
+
const attrs = match[1]
|
|
592
|
+
const typeMatch = /data-list="([^"]*)"/.exec(attrs)
|
|
593
|
+
const indentMatch = /ql-indent-(\d+)/.exec(attrs)
|
|
594
|
+
items.push({
|
|
595
|
+
type: typeMatch && typeMatch[1] === 'ordered' ? 'ordered' : 'bullet',
|
|
596
|
+
indent: indentMatch ? parseInt(indentMatch[1], 10) : 0,
|
|
597
|
+
html: match[2].replace(/^\s+|\s+$/g, ''),
|
|
598
|
+
})
|
|
599
|
+
match = pattern.exec(innerHtml)
|
|
600
|
+
}
|
|
601
|
+
if (!items.length) {
|
|
602
|
+
return ''
|
|
603
|
+
}
|
|
604
|
+
const render = (from: number, level: number): { html: string; next: number } => {
|
|
605
|
+
let html = ''
|
|
606
|
+
let i = from
|
|
607
|
+
let openType = ''
|
|
608
|
+
const close = (): void => {
|
|
609
|
+
if (openType) {
|
|
610
|
+
html += openType === 'ordered' ? '</ol>' : '</ul>'
|
|
611
|
+
openType = ''
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
while (i < items.length) {
|
|
615
|
+
const item = items[i]
|
|
616
|
+
if (item.indent < level) {
|
|
617
|
+
break
|
|
618
|
+
}
|
|
619
|
+
if (item.indent > level) {
|
|
620
|
+
// A deeper line with nothing above it reads as this level.
|
|
621
|
+
item.indent = level
|
|
622
|
+
}
|
|
623
|
+
if (openType !== item.type) {
|
|
624
|
+
close()
|
|
625
|
+
openType = item.type
|
|
626
|
+
html += openType === 'ordered' ? '<ol>' : '<ul>'
|
|
627
|
+
}
|
|
628
|
+
html += '<li>' + item.html
|
|
629
|
+
i += 1
|
|
630
|
+
if (i < items.length && items[i].indent > level) {
|
|
631
|
+
const nested = render(i, level + 1)
|
|
632
|
+
html += nested.html
|
|
633
|
+
i = nested.next
|
|
634
|
+
}
|
|
635
|
+
html += '</li>'
|
|
636
|
+
}
|
|
637
|
+
close()
|
|
638
|
+
return { html, next: i }
|
|
639
|
+
}
|
|
640
|
+
return render(0, 0).html
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/** The `<pre><code>` that a run of the studio's code-block line divs means. */
|
|
644
|
+
export function rebuildQuillCodeBlockHtml(innerHtml: string): string {
|
|
645
|
+
const lines: string[] = []
|
|
646
|
+
let language = ''
|
|
647
|
+
const pattern = /<div class="ql-code-block"([^>]*)>([\s\S]*?)<\/div>/g
|
|
648
|
+
let match = pattern.exec(innerHtml)
|
|
649
|
+
while (match) {
|
|
650
|
+
const languageMatch = /data-language="([^"]*)"/.exec(match[1])
|
|
651
|
+
if (languageMatch && !language) {
|
|
652
|
+
language = languageMatch[1]
|
|
653
|
+
}
|
|
654
|
+
lines.push(match[2] === '<br>' ? '' : match[2])
|
|
655
|
+
match = pattern.exec(innerHtml)
|
|
656
|
+
}
|
|
657
|
+
return buildCodeBlockHtml(language, lines.join('\n'), true)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* One `<blockquote>` of paragraphs out of a run of single-line quotes (one line
|
|
662
|
+
* is a run too). A run whose lines already hold blocks is stored HTML and is
|
|
663
|
+
* left alone.
|
|
664
|
+
*/
|
|
665
|
+
export function mergeBlockquoteLines(run: string): string {
|
|
666
|
+
const lines: string[] = []
|
|
667
|
+
const pattern = /<blockquote>([\s\S]*?)<\/blockquote>/g
|
|
668
|
+
let match = pattern.exec(run)
|
|
669
|
+
while (match) {
|
|
670
|
+
if (/<(?:p|ul|ol|div|pre|table|aside|figure|h[1-6])\b/.test(match[1])) {
|
|
671
|
+
return run
|
|
672
|
+
}
|
|
673
|
+
lines.push(match[1])
|
|
674
|
+
match = pattern.exec(run)
|
|
675
|
+
}
|
|
676
|
+
return '<blockquote>' + lines.map((line) => '<p>' + line + '</p>').join('') + '</blockquote>'
|
|
677
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UIDLAuthentication } from '@teleporthq/teleport-types'
|
|
2
|
+
import { CONTENT_COMMENTS_TABLES } from './content-tables'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Which store tables a browser may touch through the generated data routes.
|
|
@@ -80,8 +81,9 @@ export const CUSTOMER_RECORD_TABLES: ReadonlyArray<string> = [
|
|
|
80
81
|
'teleport_push_subscriptions',
|
|
81
82
|
// A comment is public once approved, but the row carries the commenter's
|
|
82
83
|
// email and the comments still waiting for approval; the post page reads
|
|
83
|
-
// the approved ones server-side, public columns only.
|
|
84
|
-
|
|
84
|
+
// the approved ones server-side, public columns only. One table per content
|
|
85
|
+
// preset that takes comments.
|
|
86
|
+
...CONTENT_COMMENTS_TABLES,
|
|
85
87
|
// What visitors told the AI assistant, and the merchant's private knowledge
|
|
86
88
|
// base it answers from: the assistant reads them in its own server
|
|
87
89
|
// segments, the admin lists them with its session.
|