smartrte-core 1.0.0-beta.2 → 1.0.0-beta.3
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 +4 -0
- package/dist/foundation/atom/formats.d.ts +1 -1
- package/dist/foundation/atom/formats.js +41 -3
- package/dist/foundation/atom/schema.js +29 -0
- package/dist/foundation/list/formats.js +45 -0
- package/dist/foundation/modelDom.js +18 -0
- package/dist/foundation/surface/renderer.js +56 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.3
|
|
4
|
+
|
|
5
|
+
- Add `href`/`target` (a clickable link), `borderRadius` (corner rounding), and license metadata fields (`licenseDescription`, `licenseSourceUrl`, `licenseType`, `licenseVersion`, `licenseAttribution`) to the image atom schema. `href` is validated the same way as the existing `link` mark; exported HTML wraps a linked image in a real `<a>` for portability outside the editor. Fix a pre-existing renderer gap where clearing an image's `align` back to unset never removed the CSS float/display/margin it had previously applied.
|
|
6
|
+
|
|
3
7
|
## 1.0.0-beta.2
|
|
4
8
|
|
|
5
9
|
- Add an opt-in `renderFormulaHtml` option to `serializeCanonicalListHtml` that bakes real KaTeX-rendered HTML into exported formula elements, instead of leaving an empty placeholder — for consumers (e.g. a read-only preview) that display this HTML directly without also running KaTeX against it themselves. Off by default; no change for existing consumers.
|
|
@@ -3,7 +3,7 @@ import type { SmartElementNode } from "../types.js";
|
|
|
3
3
|
export declare const atomToHtml: (node: SmartElementNode, options?: {
|
|
4
4
|
renderFormulaHtml?: boolean;
|
|
5
5
|
}) => string;
|
|
6
|
-
export declare const atomFromHtmlElement: (
|
|
6
|
+
export declare const atomFromHtmlElement: (rawElement: Element) => SmartElementNode | null;
|
|
7
7
|
export declare const atomToMarkdown: (node: SmartElementNode) => string;
|
|
8
8
|
export interface AtomDocxRun {
|
|
9
9
|
readonly kind: "image" | "text";
|
|
@@ -6,6 +6,7 @@ import katex from "katex";
|
|
|
6
6
|
// fall back to plain text in the exported/baked HTML.
|
|
7
7
|
import "katex/contrib/mhchem";
|
|
8
8
|
import { createNodeId } from "../identity.js";
|
|
9
|
+
import { sanitizeLinkHref, sanitizeLinkTarget } from "../security/urlPolicy.js";
|
|
9
10
|
import { sanitizeAtomSource } from "./security.js";
|
|
10
11
|
const escape = (value) => String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
11
12
|
const attr = (name, value) => value === undefined ? "" : ` ${name}="${escape(value)}"`;
|
|
@@ -25,7 +26,23 @@ const renderFormulaToHtml = (source) => {
|
|
|
25
26
|
export const atomToHtml = (node, options) => {
|
|
26
27
|
if (node.type === "image" || node.type === "block_image") {
|
|
27
28
|
const src = sanitizeAtomSource(String(node.attrs?.src || ""), { kind: "image", allowBlobPreview: node.attrs?.status === "pending" }) || "";
|
|
28
|
-
|
|
29
|
+
// Corner radius and license fields are data-attributes only (round-trip
|
|
30
|
+
// fidelity), matching this same function's existing data-smart-align -
|
|
31
|
+
// a raw HTML consumer displaying this string directly without also
|
|
32
|
+
// running the live surface renderer's own style logic against it
|
|
33
|
+
// wouldn't see visual rounding either way, same as align today; that's
|
|
34
|
+
// a deliberate, pre-existing scope boundary this doesn't change.
|
|
35
|
+
const img = `<img data-smart-id="${escape(node.id)}" data-smart-type="${node.type}" src="${escape(src)}" alt="${escape(node.attrs?.alt)}"${dimensions(node)}${attr("data-smart-status", node.attrs?.status || "ready")}${node.attrs?.decorative === true ? ' data-smart-decorative="true"' : ""}${attr("data-smart-align", node.attrs?.align)}${attr("data-smart-radius", node.attrs?.borderRadius)}${attr("data-smart-license-description", node.attrs?.licenseDescription)}${attr("data-smart-license-source-url", node.attrs?.licenseSourceUrl)}${attr("data-smart-license-type", node.attrs?.licenseType)}${attr("data-smart-license-version", node.attrs?.licenseVersion)}${attr("data-smart-license-attribution", node.attrs?.licenseAttribution)}${attr("data-smart-href", node.attrs?.href)}${attr("data-smart-target", node.attrs?.target)}>`;
|
|
36
|
+
// A real <a> wrapper, unlike the live surface renderer's plain data
|
|
37
|
+
// attributes on the bare <img> - see surface/renderer.ts's own
|
|
38
|
+
// reasoning (stable-element-per-id constraint that doesn't apply to
|
|
39
|
+
// this string-based export) and modelDom.ts's identical choice for the
|
|
40
|
+
// one-shot clipboard/print DOM builder.
|
|
41
|
+
const linkHref = sanitizeLinkHref(typeof node.attrs?.href === "string" ? node.attrs.href : undefined);
|
|
42
|
+
if (!linkHref)
|
|
43
|
+
return img;
|
|
44
|
+
const target = sanitizeLinkTarget(typeof node.attrs?.target === "string" ? node.attrs.target : undefined);
|
|
45
|
+
return `<a href="${escape(linkHref)}"${target ? ` target="${escape(target)}"` : ""}>${img}</a>`;
|
|
29
46
|
}
|
|
30
47
|
if (node.type === "formula" || node.type === "block_formula") {
|
|
31
48
|
const tag = node.type === "formula" ? "span" : "div";
|
|
@@ -48,7 +65,14 @@ export const atomToHtml = (node, options) => {
|
|
|
48
65
|
return `<hr data-smart-id="${escape(node.id)}" data-smart-type="divider">`;
|
|
49
66
|
throw new Error(`Unsupported atom type "${node.type}".`);
|
|
50
67
|
};
|
|
51
|
-
export const atomFromHtmlElement = (
|
|
68
|
+
export const atomFromHtmlElement = (rawElement) => {
|
|
69
|
+
// atomToHtml's own inverse - a linked image now wraps in a real <a>
|
|
70
|
+
// (needed so the link survives outside a live editor at all, see
|
|
71
|
+
// atomToHtml's own doc comment); unwrap to the actual atom element
|
|
72
|
+
// (which still carries every attribute, including data-smart-href,
|
|
73
|
+
// directly) before parsing, matching what a single element's own
|
|
74
|
+
// attributes already fully describe.
|
|
75
|
+
const element = rawElement.tagName === "A" && rawElement.children.length === 1 ? rawElement.children[0] : rawElement;
|
|
52
76
|
const declared = element.getAttribute("data-smart-type");
|
|
53
77
|
const type = declared || (element.tagName === "IMG" ? "image" : element.tagName.toLowerCase());
|
|
54
78
|
const id = element.getAttribute("data-smart-id") || createNodeId();
|
|
@@ -57,7 +81,21 @@ export const atomFromHtmlElement = (element) => {
|
|
|
57
81
|
const src = sanitizeAtomSource(element.getAttribute("src"), { kind: "image" });
|
|
58
82
|
if (!src)
|
|
59
83
|
return null;
|
|
60
|
-
|
|
84
|
+
const radius = Number(element.getAttribute("data-smart-radius"));
|
|
85
|
+
return { type, id, attrs: {
|
|
86
|
+
src, alt: element.getAttribute("alt") || "", status: element.getAttribute("data-smart-status") || "ready",
|
|
87
|
+
...(element.getAttribute("data-smart-decorative") === "true" ? { decorative: true } : {}),
|
|
88
|
+
...(element.getAttribute("data-smart-align") ? { align: element.getAttribute("data-smart-align") } : {}),
|
|
89
|
+
...(number("width") ? { width: number("width") } : {}), ...(number("height") ? { height: number("height") } : {}),
|
|
90
|
+
...(Number.isFinite(radius) && radius > 0 ? { borderRadius: radius } : {}),
|
|
91
|
+
...(element.getAttribute("data-smart-href") ? { href: element.getAttribute("data-smart-href") } : {}),
|
|
92
|
+
...(element.getAttribute("data-smart-target") ? { target: element.getAttribute("data-smart-target") } : {}),
|
|
93
|
+
...(element.getAttribute("data-smart-license-description") ? { licenseDescription: element.getAttribute("data-smart-license-description") } : {}),
|
|
94
|
+
...(element.getAttribute("data-smart-license-source-url") ? { licenseSourceUrl: element.getAttribute("data-smart-license-source-url") } : {}),
|
|
95
|
+
...(element.getAttribute("data-smart-license-type") ? { licenseType: element.getAttribute("data-smart-license-type") } : {}),
|
|
96
|
+
...(element.getAttribute("data-smart-license-version") ? { licenseVersion: element.getAttribute("data-smart-license-version") } : {}),
|
|
97
|
+
...(element.getAttribute("data-smart-license-attribution") ? { licenseAttribution: element.getAttribute("data-smart-license-attribution") } : {}),
|
|
98
|
+
} };
|
|
61
99
|
}
|
|
62
100
|
if (type === "formula" || type === "block_formula") {
|
|
63
101
|
return { type, id, attrs: { source: element.getAttribute("data-smart-formula") || "", notation: element.getAttribute("data-smart-notation") === "mathml" ? "mathml" : "latex" } };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeLinkInput } from "../security/urlPolicy.js";
|
|
1
2
|
const optionalString = { validate: (value) => typeof value === "string" };
|
|
2
3
|
const requiredString = { required: true, validate: (value) => typeof value === "string" };
|
|
3
4
|
const dimension = { validate: (value) => Number.isFinite(value) && Number(value) > 0 && Number(value) <= 100000 };
|
|
@@ -6,6 +7,34 @@ const imageAttrs = {
|
|
|
6
7
|
src: requiredString, alt: requiredString, width: dimension, height: dimension,
|
|
7
8
|
status, uploadId: optionalString, error: optionalString, decorative: { validate: (value) => typeof value === "boolean" },
|
|
8
9
|
align: { validate: (value) => value === "center" || value === "left" || value === "right" },
|
|
10
|
+
/**
|
|
11
|
+
* docs/bugs/media-details-old-editor-field-parity.md: the old editor's
|
|
12
|
+
* "Link"/"Target" fields have no equivalent yet - the `link` mark already
|
|
13
|
+
* has this exact href/target shape (marks/commands.ts), but marks are
|
|
14
|
+
* explicitly disallowed on every atom node (`marks: ""` below), so this
|
|
15
|
+
* is a dedicated pair of atom-level attrs instead of reusing the mark.
|
|
16
|
+
*/
|
|
17
|
+
/** Same normalizeLinkInput validation the "link" mark's own schema uses (marks/schema.ts) - rejects at the command layer, not just sanitizing later at render/export time. */
|
|
18
|
+
href: { validate: (value) => typeof value === "string" && normalizeLinkInput(value).href !== null },
|
|
19
|
+
target: optionalString,
|
|
20
|
+
/** Corner rounding (px) - confirmed absent anywhere in this schema before now, unlike align/alt/width which already existed with no UI. */
|
|
21
|
+
borderRadius: { validate: (value) => Number.isFinite(value) && Number(value) >= 0 && Number(value) <= 1000 },
|
|
22
|
+
/**
|
|
23
|
+
* Mirrors MediaItem.license's own shape (packages/react/src/mediaProvider.ts)
|
|
24
|
+
* field-for-field where a real equivalent exists, so a library-sourced
|
|
25
|
+
* image's already-fetched license metadata can be copied straight across
|
|
26
|
+
* at insert time (CanonicalAuthorityEditor.tsx's selectFromMediaManager)
|
|
27
|
+
* instead of being read, shown in the library browser, and then silently
|
|
28
|
+
* discarded the way it was before this - the atom schema had nowhere to
|
|
29
|
+
* persist it. Renamed to this project's own vocabulary (the old editor's
|
|
30
|
+
* "Attribution text" ~ MediaItem.license.author, "License description" ~
|
|
31
|
+
* workName/licenseText) and kept independently user-editable, since a
|
|
32
|
+
* freshly uploaded image has no provider-supplied license data at all.
|
|
33
|
+
* `licenseVersion` has no MediaItem.license equivalent (its shape has no
|
|
34
|
+
* separate version field) - always user-entered, never auto-populated.
|
|
35
|
+
*/
|
|
36
|
+
licenseDescription: optionalString, licenseSourceUrl: optionalString,
|
|
37
|
+
licenseType: optionalString, licenseVersion: optionalString, licenseAttribution: optionalString,
|
|
9
38
|
};
|
|
10
39
|
const formulaAttrs = {
|
|
11
40
|
source: requiredString,
|
|
@@ -20,6 +20,23 @@ const isEditorUiNode = (node) => attr(node, "data-smart-ui") !== undefined || at
|
|
|
20
20
|
* parseBlockList below for how they're unwrapped instead of swallowed.
|
|
21
21
|
*/
|
|
22
22
|
const TRANSPARENT_CONTAINER_TAGS = ["div", "section", "article", "figure"];
|
|
23
|
+
/** Shared by both the inline `image` and block `block_image` parsing branches below - radius/license round-trip identically for both, only href/target's link-mark-fallback behavior deliberately differs between them (see each call site's own comment). */
|
|
24
|
+
const imageStyleAndLicenseAttrs = (node) => {
|
|
25
|
+
const radius = Number(attr(node, "data-smart-radius"));
|
|
26
|
+
const licenseDescription = attr(node, "data-smart-license-description");
|
|
27
|
+
const licenseSourceUrl = attr(node, "data-smart-license-source-url");
|
|
28
|
+
const licenseType = attr(node, "data-smart-license-type");
|
|
29
|
+
const licenseVersion = attr(node, "data-smart-license-version");
|
|
30
|
+
const licenseAttribution = attr(node, "data-smart-license-attribution");
|
|
31
|
+
return {
|
|
32
|
+
...(Number.isFinite(radius) && radius > 0 ? { borderRadius: radius } : {}),
|
|
33
|
+
...(licenseDescription ? { licenseDescription } : {}),
|
|
34
|
+
...(licenseSourceUrl ? { licenseSourceUrl } : {}),
|
|
35
|
+
...(licenseType ? { licenseType } : {}),
|
|
36
|
+
...(licenseVersion ? { licenseVersion } : {}),
|
|
37
|
+
...(licenseAttribution ? { licenseAttribution } : {}),
|
|
38
|
+
};
|
|
39
|
+
};
|
|
23
40
|
/** See serializeCanonicalListHtml's own doc comment for why this is a module-scoped flag rather than a threaded parameter. */
|
|
24
41
|
let renderFormulaHtmlMode = false;
|
|
25
42
|
const serializeInline = (node) => {
|
|
@@ -227,12 +244,27 @@ const textWithMarks = (node, inherited = []) => {
|
|
|
227
244
|
// synchronous and the image hasn't loaded yet at this point.
|
|
228
245
|
const width = parsePixelWidth(styleValue(node, "width")) ?? parsePixelWidth(attr(node, "width"));
|
|
229
246
|
const height = parsePixelWidth(styleValue(node, "height")) ?? parsePixelWidth(attr(node, "height"));
|
|
247
|
+
// <a><img></a> (this project's own export - list/formats.ts's own
|
|
248
|
+
// serializeBlock/atom/formats.ts's atomToHtml wrap a linked image in a
|
|
249
|
+
// real anchor - or any real-world source doing the same): the link
|
|
250
|
+
// mark from that ancestor <a> arrives here via `inherited`, but
|
|
251
|
+
// SmartElementNode has no `marks` field at all (only SmartTextNode
|
|
252
|
+
// does) - without this, the href would be silently discarded the
|
|
253
|
+
// moment it reached this atomic-image branch. The live editor's own
|
|
254
|
+
// DOM instead encodes this as plain data-smart-href/-target
|
|
255
|
+
// attributes directly on the <img> (surface/renderer.ts) with no
|
|
256
|
+
// wrapping <a> at all, so both encodings are checked.
|
|
257
|
+
const inheritedLink = inherited.find((mark) => mark.type === "link");
|
|
258
|
+
const href = attr(node, "data-smart-href") || inheritedLink?.attrs?.href;
|
|
259
|
+
const target = attr(node, "data-smart-target") || inheritedLink?.attrs?.target;
|
|
230
260
|
return [{ type: "image", id: generatedId(node, "image"), attrs: {
|
|
231
261
|
src, alt: attr(node, "alt") || "", status: attr(node, "data-smart-status") || "ready",
|
|
232
262
|
...(attr(node, "data-smart-decorative") === "true" ? { decorative: true } : {}),
|
|
233
263
|
...(attr(node, "title") ? { title: attr(node, "title") } : {}),
|
|
234
264
|
...(attr(node, "data-smart-align") ? { align: attr(node, "data-smart-align") } : {}),
|
|
235
265
|
...(width !== null ? { width } : {}), ...(height !== null ? { height } : {}),
|
|
266
|
+
...(href ? { href } : {}), ...(target ? { target } : {}),
|
|
267
|
+
...imageStyleAndLicenseAttrs(node),
|
|
236
268
|
} }];
|
|
237
269
|
}
|
|
238
270
|
}
|
|
@@ -375,6 +407,19 @@ const parseBlock = (node) => {
|
|
|
375
407
|
src, alt: attr(node, "alt") || "", status: attr(node, "data-smart-status") || "ready",
|
|
376
408
|
...(attr(node, "data-smart-decorative") === "true" ? { decorative: true } : {}),
|
|
377
409
|
...(width !== null ? { width } : {}), ...(height !== null ? { height } : {}),
|
|
410
|
+
// Only from data-smart-href/-target directly on this element (our own
|
|
411
|
+
// round-tripped export, per this whole branch's own `declaredAtom`
|
|
412
|
+
// gate) - deliberately NOT also checking for a wrapping <a> the way
|
|
413
|
+
// the inline `image` atom's parsing does just above in textWithMarks.
|
|
414
|
+
// The bare-<img>-at-block-level fallback a few lines down has its own
|
|
415
|
+
// long-standing, evidence-based decision (a real Wikipedia "copy
|
|
416
|
+
// image" paste) to unwrap and drop an incidental <a> wrapper rather
|
|
417
|
+
// than treat it as a user-authored link - extending that to also
|
|
418
|
+
// read href here would silently reverse that decision for every
|
|
419
|
+
// third-party block-level image paste, not just this app's own.
|
|
420
|
+
...(attr(node, "data-smart-href") ? { href: attr(node, "data-smart-href") } : {}),
|
|
421
|
+
...(attr(node, "data-smart-target") ? { target: attr(node, "data-smart-target") } : {}),
|
|
422
|
+
...imageStyleAndLicenseAttrs(node),
|
|
378
423
|
} };
|
|
379
424
|
}
|
|
380
425
|
if (declaredAtom === "block_formula")
|
|
@@ -2,6 +2,7 @@ import { isTextNode } from "./identity.js";
|
|
|
2
2
|
import { nodeAtPath } from "./positions.js";
|
|
3
3
|
import { renderMarkedText } from "./marks/dom.js";
|
|
4
4
|
import { sanitizeAtomSource } from "./atom/security.js";
|
|
5
|
+
import { sanitizeLinkHref, sanitizeLinkTarget } from "./security/urlPolicy.js";
|
|
5
6
|
const atomTypes = new Set(["image", "block_image", "formula", "block_formula", "video", "audio", "divider"]);
|
|
6
7
|
export const SMART_UI_ATTRIBUTE = "data-smart-ui";
|
|
7
8
|
export const SMART_PROJECTION_ATTRIBUTE = "data-smart-projection";
|
|
@@ -93,6 +94,23 @@ const renderNode = (node, ownerDocument) => {
|
|
|
93
94
|
if (source)
|
|
94
95
|
element.setAttribute("src", source);
|
|
95
96
|
element.setAttribute("alt", typeof node.attrs?.alt === "string" ? node.attrs.alt : "");
|
|
97
|
+
if (node.attrs?.borderRadius)
|
|
98
|
+
element.style.borderRadius = `${Number(node.attrs.borderRadius)}px`;
|
|
99
|
+
const linkHref = sanitizeLinkHref(typeof node.attrs?.href === "string" ? node.attrs.href : undefined);
|
|
100
|
+
if (linkHref) {
|
|
101
|
+
// A real <a> wrapper, unlike the live surface renderer's plain data
|
|
102
|
+
// attributes - this is a one-shot tree builder (no per-node element
|
|
103
|
+
// identity to preserve across renders), and clipboard/print output
|
|
104
|
+
// needs a genuine anchor for the link to survive outside this editor
|
|
105
|
+
// at all.
|
|
106
|
+
const link = ownerDocument.createElement("a");
|
|
107
|
+
link.setAttribute("href", linkHref);
|
|
108
|
+
const linkTarget = sanitizeLinkTarget(typeof node.attrs?.target === "string" ? node.attrs.target : undefined);
|
|
109
|
+
if (linkTarget)
|
|
110
|
+
link.setAttribute("target", linkTarget);
|
|
111
|
+
link.appendChild(element);
|
|
112
|
+
return link;
|
|
113
|
+
}
|
|
96
114
|
}
|
|
97
115
|
if (node.type === "formula" || node.type === "block_formula") {
|
|
98
116
|
const source = String(node.attrs?.source || "");
|
|
@@ -405,6 +405,62 @@ export class FoundationSubtreeRenderer {
|
|
|
405
405
|
element.style.float = imageAlign;
|
|
406
406
|
element.style.margin = imageAlign === "left" ? "0 8px 8px 0" : "0 0 8px 8px";
|
|
407
407
|
}
|
|
408
|
+
else {
|
|
409
|
+
// No align set - reset every property the two branches above can
|
|
410
|
+
// set, not just skip setting new ones. A pre-existing gap (found
|
|
411
|
+
// while verifying docs/bugs/media-details-old-editor-field-parity.md's
|
|
412
|
+
// new "clear align back to None" UI path): this renderer's other
|
|
413
|
+
// conditional style branches (e.g. borderRadius just below) always
|
|
414
|
+
// pair their "set" case with an explicit "else remove" case for
|
|
415
|
+
// exactly this reason - align's own else branches never existed
|
|
416
|
+
// before this UI made "align was set, then explicitly cleared" a
|
|
417
|
+
// real, reachable transition for the first time.
|
|
418
|
+
element.style.removeProperty("display");
|
|
419
|
+
element.style.removeProperty("float");
|
|
420
|
+
element.style.removeProperty("margin");
|
|
421
|
+
}
|
|
422
|
+
if (node.attrs?.borderRadius)
|
|
423
|
+
element.style.borderRadius = `${Number(node.attrs.borderRadius)}px`;
|
|
424
|
+
else
|
|
425
|
+
element.style.removeProperty("border-radius");
|
|
426
|
+
// Plain data attributes, not a wrapping <a> - this element's identity
|
|
427
|
+
// is stable across renders (see this class's own diffing contract);
|
|
428
|
+
// introducing a conditional DOM-structure change here for something
|
|
429
|
+
// input.ts's click handling already reads directly is unnecessary
|
|
430
|
+
// risk. atomToHtml/modelDom.ts DO wrap in a real <a> for the
|
|
431
|
+
// string/one-shot export paths, which have no such stability
|
|
432
|
+
// constraint and need a real anchor for the link to work outside
|
|
433
|
+
// this live editor at all.
|
|
434
|
+
if (typeof node.attrs?.href === "string" && node.attrs.href)
|
|
435
|
+
this.setAttribute(element, "data-smart-href", node.attrs.href, node.id);
|
|
436
|
+
else
|
|
437
|
+
this.removeAttribute(element, "data-smart-href", node.id);
|
|
438
|
+
if (typeof node.attrs?.target === "string" && node.attrs.target)
|
|
439
|
+
this.setAttribute(element, "data-smart-target", node.attrs.target, node.id);
|
|
440
|
+
else
|
|
441
|
+
this.removeAttribute(element, "data-smart-target", node.id);
|
|
442
|
+
// Mirrored as data attributes too, matching href/target above - a
|
|
443
|
+
// host inspecting the live DOM (or this project's own e2e suite)
|
|
444
|
+
// should be able to see this data the same way it can see href, not
|
|
445
|
+
// just find it by reading the model directly. Purely descriptive
|
|
446
|
+
// metadata, so unlike align/borderRadius there's no matching CSS
|
|
447
|
+
// side effect to apply here.
|
|
448
|
+
if (node.attrs?.borderRadius)
|
|
449
|
+
this.setAttribute(element, "data-smart-radius", String(node.attrs.borderRadius), node.id);
|
|
450
|
+
else
|
|
451
|
+
this.removeAttribute(element, "data-smart-radius", node.id);
|
|
452
|
+
[
|
|
453
|
+
["licenseDescription", "data-smart-license-description"],
|
|
454
|
+
["licenseSourceUrl", "data-smart-license-source-url"],
|
|
455
|
+
["licenseType", "data-smart-license-type"],
|
|
456
|
+
["licenseVersion", "data-smart-license-version"],
|
|
457
|
+
["licenseAttribution", "data-smart-license-attribution"],
|
|
458
|
+
].forEach(([key, attrName]) => {
|
|
459
|
+
if (typeof node.attrs?.[key] === "string" && node.attrs[key])
|
|
460
|
+
this.setAttribute(element, attrName, String(node.attrs[key]), node.id);
|
|
461
|
+
else
|
|
462
|
+
this.removeAttribute(element, attrName, node.id);
|
|
463
|
+
});
|
|
408
464
|
}
|
|
409
465
|
else if (node.type === "formula" || node.type === "block_formula") {
|
|
410
466
|
const source = String(node.attrs?.source || "");
|