@aceshooting/lyra-ui 3.8.0 → 3.9.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/README.md +3 -2
- package/custom-elements.json +1264 -97
- package/dist/components/activity-feed/activity-feed.class.d.ts +13 -1
- package/dist/components/activity-feed/activity-feed.class.d.ts.map +1 -1
- package/dist/components/activity-feed/activity-feed.class.js +10 -2
- package/dist/components/activity-feed/activity-feed.class.js.map +1 -1
- package/dist/components/graph/graph-canvas.d.ts +3 -0
- package/dist/components/graph/graph-canvas.d.ts.map +1 -1
- package/dist/components/graph/graph-canvas.js +5 -0
- package/dist/components/graph/graph-canvas.js.map +1 -1
- package/dist/components/graph/graph.class.d.ts +14 -0
- package/dist/components/graph/graph.class.d.ts.map +1 -1
- package/dist/components/graph/graph.class.js +28 -0
- package/dist/components/graph/graph.class.js.map +1 -1
- package/dist/components/graph/graph.styles.d.ts.map +1 -1
- package/dist/components/graph/graph.styles.js +6 -0
- package/dist/components/graph/graph.styles.js.map +1 -1
- package/dist/components/input/input.class.d.ts +3 -1
- package/dist/components/input/input.class.d.ts.map +1 -1
- package/dist/components/input/input.class.js +2 -0
- package/dist/components/input/input.class.js.map +1 -1
- package/dist/components/markdown/markdown-core.class.d.ts +334 -0
- package/dist/components/markdown/markdown-core.class.d.ts.map +1 -0
- package/dist/components/markdown/markdown-core.class.js +999 -0
- package/dist/components/markdown/markdown-core.class.js.map +1 -0
- package/dist/components/markdown/markdown-core.d.ts +2 -0
- package/dist/components/markdown/markdown-core.d.ts.map +1 -0
- package/dist/components/markdown/markdown-core.js +5 -0
- package/dist/components/markdown/markdown-core.js.map +1 -0
- package/dist/components/thread-list/thread-list.class.d.ts +10 -0
- package/dist/components/thread-list/thread-list.class.d.ts.map +1 -1
- package/dist/components/thread-list/thread-list.class.js +9 -1
- package/dist/components/thread-list/thread-list.class.js.map +1 -1
- package/dist/components/usage-badge/usage-badge.class.d.ts +10 -1
- package/dist/components/usage-badge/usage-badge.class.d.ts.map +1 -1
- package/dist/components/usage-badge/usage-badge.class.js +9 -0
- package/dist/components/usage-badge/usage-badge.class.js.map +1 -1
- package/dist/internal/root-registration-allowlist.d.ts +1 -1
- package/dist/internal/root-registration-allowlist.d.ts.map +1 -1
- package/dist/internal/root-registration-allowlist.js +1 -0
- package/dist/internal/root-registration-allowlist.js.map +1 -1
- package/dist/lyra.d.ts +3 -0
- package/dist/lyra.d.ts.map +1 -1
- package/dist/lyra.js +2 -0
- package/dist/lyra.js.map +1 -1
- package/llms-full.txt +83 -9
- package/llms.txt +2 -1
- package/package.json +31 -31
- package/vscode-css-data.json +7 -0
- package/vscode-html-data.json +106 -6
- package/web-types.json +129 -7
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { html } from 'lit';
|
|
8
|
+
import { property, state } from 'lit/decorators.js';
|
|
9
|
+
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
|
10
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
11
|
+
import { srOnly } from '../../internal/a11y.js';
|
|
12
|
+
import { prefersReducedMotion } from '../../internal/motion.js';
|
|
13
|
+
import { Slugger } from '../../internal/slugger.js';
|
|
14
|
+
import { DocumentAnchorTarget } from '../../internal/anchor-target.js';
|
|
15
|
+
import { scopeFromElement, resolveTextQuote, buildQuoteAnchor } from '../../internal/text-quote.js';
|
|
16
|
+
import { acquireHighlightHandle, supportsCustomHighlights } from '../../internal/text-highlights.js';
|
|
17
|
+
import { finiteInteger } from '../../internal/numbers.js';
|
|
18
|
+
import { loadMarkdownDeps, getMarkdownDepsIfLoaded } from './markdown-loader.js';
|
|
19
|
+
import { loadShikiHighlighterCore, normalizeShikiLanguage, SHIKI_THEMES, } from '../code-block/code-loader.js';
|
|
20
|
+
import { getKatex } from './katex-loader.js';
|
|
21
|
+
import { styles } from './markdown.styles.js';
|
|
22
|
+
const HTML_ESCAPES = {
|
|
23
|
+
'&': '&',
|
|
24
|
+
'<': '<',
|
|
25
|
+
'>': '>',
|
|
26
|
+
'"': '"',
|
|
27
|
+
"'": ''',
|
|
28
|
+
};
|
|
29
|
+
function escapeHtml(text) {
|
|
30
|
+
return text.replace(/[&<>"']/g, (ch) => HTML_ESCAPES[ch] ?? ch);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Mirrors marked's own default `link()` renderer's `cleanUrl()`: a malformed
|
|
34
|
+
* percent-escape or lone UTF-16 surrogate in the raw href throws inside
|
|
35
|
+
* `encodeURI`, and marked's default renderer responds by dropping the anchor
|
|
36
|
+
* (rendering the link text alone) rather than emitting a broken `href` —
|
|
37
|
+
* returning `null` here lets the caller do the same. The
|
|
38
|
+
* `.replace(/%25/g, '%')` compensates for `encodeURI` re-escaping the `%` of
|
|
39
|
+
* an href that was already percent-encoded (the common case for a real
|
|
40
|
+
* markdown link) — without it, every existing `%XX` escape would become
|
|
41
|
+
* `%25XX`, double-encoding it.
|
|
42
|
+
*/
|
|
43
|
+
function cleanHref(href) {
|
|
44
|
+
try {
|
|
45
|
+
return encodeURI(href).replace(/%25/g, '%');
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Every `LyraHighlightTone`, used to always call `HighlightHandle.setRanges()` once per tone on
|
|
52
|
+
* every repaint (with an empty array for an unused tone) -- `setRanges()` replaces a tone's ranges
|
|
53
|
+
* wholesale per call, so a tone this pass has nothing for still needs an explicit empty call to
|
|
54
|
+
* clear whatever it painted last pass. */
|
|
55
|
+
const HIGHLIGHT_TONES = ['accent', 'success', 'warning', 'danger', 'neutral'];
|
|
56
|
+
/** Upper bound on `highlightCache` entries per instance. Each entry holds a fully-highlighted
|
|
57
|
+
* HTML string (potentially large for a long code block), and the cache is content-addressed --
|
|
58
|
+
* on a long-lived instance whose `content` keeps changing (a chat transcript, live docs), an
|
|
59
|
+
* unbounded map would retain the highlighted HTML of every code block ever rendered. 100 far
|
|
60
|
+
* exceeds the fenced-block count of any one document, so eviction only trims blocks that
|
|
61
|
+
* scrolled out of the content long ago. */
|
|
62
|
+
const HIGHLIGHT_CACHE_MAX = 100;
|
|
63
|
+
// -- math (KaTeX) -----------------------------------------------------------------------------
|
|
64
|
+
/** Resolved once per page and reused by every `<lyra-markdown>` instance, mirroring
|
|
65
|
+
* `markdown-loader.ts`'s own "warm cache" shape: `undefined` means no load has been kicked off
|
|
66
|
+
* yet, `null` a load finished but the peer isn't installed, and it's set to `null` synchronously
|
|
67
|
+
* the instant a load starts as an in-flight marker so a second concurrent instance never kicks off
|
|
68
|
+
* a duplicate `getKatex()` call. */
|
|
69
|
+
let resolvedKatex;
|
|
70
|
+
/** Whether `getKatex()` has already been kicked off, module-wide -- kept separate from
|
|
71
|
+
* `resolvedKatex` so "a load is in flight" and "a load finished and the peer is confirmed
|
|
72
|
+
* missing" stay distinguishable (both would otherwise collapse to the same falsy `null`). */
|
|
73
|
+
let katexLoadStarted = false;
|
|
74
|
+
/** Test-only override bypassing the real dynamic `import('katex')` and the module cache above
|
|
75
|
+
* entirely -- `undefined` (the default) defers to `resolvedKatex`. */
|
|
76
|
+
let katexOverride;
|
|
77
|
+
/** @internal Test-only seam: forces `math` rendering to behave as if `katex` resolved to `katex`
|
|
78
|
+
* (or, with `null`, as if the optional peer failed to load). Pass `undefined` to restore the real
|
|
79
|
+
* `getKatex()`-driven behavior. */
|
|
80
|
+
export function __setKatexForTesting(katex) {
|
|
81
|
+
katexOverride = katex;
|
|
82
|
+
}
|
|
83
|
+
function getKatexIfLoaded() {
|
|
84
|
+
return katexOverride !== undefined ? katexOverride : (resolvedKatex ?? null);
|
|
85
|
+
}
|
|
86
|
+
/** Whether the `katex` load has definitively finished with no peer available -- distinct from
|
|
87
|
+
* `getKatexIfLoaded()` returning falsy, which also covers a load that's merely still in flight
|
|
88
|
+
* (the state a math token's render-time literal fallback uses regardless of which case it is).
|
|
89
|
+
* Used only to decide whether a literal fallback should also report `lyra-render-error`. */
|
|
90
|
+
function isKatexConfirmedMissing() {
|
|
91
|
+
return (katexOverride !== undefined ? katexOverride : resolvedKatex) === null;
|
|
92
|
+
}
|
|
93
|
+
const MATH_BLOCK_RE = /^\$\$([^\n]+?)\$\$/;
|
|
94
|
+
const MATH_INLINE_RE = /^\$((?:\\\$|[^$\s])(?:\\\$|[^$])*?)\$(?!\$)/;
|
|
95
|
+
/** A marked inline-level extension recognizing `$...$` and `$$...$$` TeX. Tokenizing is
|
|
96
|
+
* synchronous (matching marked's own parse pass); actual KaTeX rendering happens in `renderMath`
|
|
97
|
+
* against whatever `getKatexIfLoaded()` already has resolved -- mirroring this component's
|
|
98
|
+
* existing two-phase pattern for fenced-code highlighting (`code()` returns a plain placeholder
|
|
99
|
+
* first; the real highlighted markup arrives one render later once shiki resolves). `renderer` is
|
|
100
|
+
* bundled directly into this same extension object (not marked's separate top-level `.use({
|
|
101
|
+
* renderer })` override hook -- that hook only recognizes the *built-in* renderer method names;
|
|
102
|
+
* a custom token type introduced via `.use({ extensions })` supplies its own renderer the same way). */
|
|
103
|
+
function mathExtension(renderMath) {
|
|
104
|
+
return {
|
|
105
|
+
name: 'math',
|
|
106
|
+
level: 'inline',
|
|
107
|
+
start(src) {
|
|
108
|
+
const index = src.indexOf('$');
|
|
109
|
+
return index === -1 ? undefined : index;
|
|
110
|
+
},
|
|
111
|
+
tokenizer(src) {
|
|
112
|
+
const block = MATH_BLOCK_RE.exec(src);
|
|
113
|
+
if (block)
|
|
114
|
+
return { type: 'math', raw: block[0], tex: block[1].trim(), display: true };
|
|
115
|
+
const inline = MATH_INLINE_RE.exec(src);
|
|
116
|
+
if (inline)
|
|
117
|
+
return { type: 'math', raw: inline[0], tex: inline[1].replace(/\\\$/g, '$').trim(), display: false };
|
|
118
|
+
return undefined;
|
|
119
|
+
},
|
|
120
|
+
renderer(token) {
|
|
121
|
+
return renderMath(token);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Rewrites shiki's generated `<pre>`/`<code>` hast nodes so the highlighted output keeps
|
|
127
|
+
* `<lyra-markdown>`'s own `part="code-block"` hook and a `language-${lang}` class on `<code>` --
|
|
128
|
+
* matching today's plain-render output shape exactly, so existing consumer CSS targeting either
|
|
129
|
+
* keeps working whether or not a given block ended up highlighted. A separate, purpose-built
|
|
130
|
+
* function from `code-block.class.ts`'s own (private, non-exported) `partTransformer` -- that one
|
|
131
|
+
* targets `<lyra-code-block>`'s own `part="pre"`/`part="code"`/line-numbers contract, which doesn't
|
|
132
|
+
* apply here.
|
|
133
|
+
*/
|
|
134
|
+
function markdownCodeTransformer(lang) {
|
|
135
|
+
return {
|
|
136
|
+
name: 'lyra-markdown-code-block',
|
|
137
|
+
pre(node) {
|
|
138
|
+
node.properties.part = ['code-block'];
|
|
139
|
+
delete node.properties.tabindex;
|
|
140
|
+
},
|
|
141
|
+
code(node) {
|
|
142
|
+
const classes = Array.isArray(node.properties.class)
|
|
143
|
+
? node.properties.class
|
|
144
|
+
: node.properties.class
|
|
145
|
+
? [node.properties.class]
|
|
146
|
+
: [];
|
|
147
|
+
node.properties.class = [...classes, `language-${lang}`];
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
class LyraMarkdownCoreBase extends LyraElement {
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* `<lyra-markdown-core>` — a build-lean variant of `<lyra-markdown>` for a consumer whose
|
|
155
|
+
* `languages` map already covers every language it will ever render. Every other capability (GFM
|
|
156
|
+
* tables, fenced code blocks, links, blockquotes, heading anchors, text-quote highlights, math) is
|
|
157
|
+
* identical to `<lyra-markdown>` -- only fenced-code-block highlighting differs: this component's
|
|
158
|
+
* own module never textually contains a call to (or import of) `loadShikiHighlighter` (the
|
|
159
|
+
* ~200-language default dynamic-import table `<lyra-markdown>` calls unconditionally unless its
|
|
160
|
+
* *runtime* `languagesOnly` flag is `true` -- a flag on that same module a bundler can't prove
|
|
161
|
+
* always-true, so the unconditional call stays in the build output regardless). A consumer
|
|
162
|
+
* importing this entry point instead of `markdown.js` gets a genuinely shiki-full-table-free
|
|
163
|
+
* build. A fenced block whose language isn't a key in `languages` always renders the plain-text
|
|
164
|
+
* fallback -- there is no default/full-table highlighter here to fall back to, mirroring
|
|
165
|
+
* `<lyra-code-block-core>`'s identical contract for the sibling component.
|
|
166
|
+
*
|
|
167
|
+
* Built on the optional peer dependencies `marked` (parsing) and `dompurify` (sanitizing), both
|
|
168
|
+
* lazy-loaded via `markdown-loader.ts` on first connect.
|
|
169
|
+
*
|
|
170
|
+
* Rendering never ships unsanitized or broken markup silently:
|
|
171
|
+
* - If `marked` fails to load, or throws while parsing malformed input, the
|
|
172
|
+
* component falls back to plain text (`white-space: pre-wrap`, no HTML
|
|
173
|
+
* parsing at all) and fires `lyra-render-error`.
|
|
174
|
+
* - If `sanitize` is `true` (the default) and `dompurify` fails to load, the
|
|
175
|
+
* component *also* falls back to plain text + `lyra-render-error` — it
|
|
176
|
+
* never renders marked's raw HTML output when sanitization was requested
|
|
177
|
+
* (or defaulted to) but is unavailable, even though `marked` itself loaded
|
|
178
|
+
* fine.
|
|
179
|
+
* - If `sanitize` is explicitly `false`, marked's raw output renders as-is
|
|
180
|
+
* regardless of whether `dompurify` is installed — the consumer opted out
|
|
181
|
+
* of sanitization, so `dompurify`'s absence is irrelevant to that path.
|
|
182
|
+
*
|
|
183
|
+
* That same plain-text fallback rendering (`data-fallback` on the `content`
|
|
184
|
+
* part) is also, unconditionally and by default, a brief *transient* state on
|
|
185
|
+
* every connect, not just a failure path: `connectedCallback()`'s dynamic
|
|
186
|
+
* `import()` of `marked`/`dompurify` (see `markdown-loader.ts`) is
|
|
187
|
+
* asynchronous, so the very first paint of any `<lyra-markdown>` on a page
|
|
188
|
+
* shows plain text for at least one microtask — even when both peers are
|
|
189
|
+
* already installed and load without error — until that import resolves and
|
|
190
|
+
* a second render replaces it with the real Markdown output. Set
|
|
191
|
+
* `eager-load` to skip that window once the shared dependency cache is
|
|
192
|
+
* already warm; see that property's doc for exactly what "warm" requires.
|
|
193
|
+
*
|
|
194
|
+
* `heading`/`code`/`blockquote`/`table`/`link`/`image` tokens are rendered
|
|
195
|
+
* through a `marked` renderer override that injects `part="..."` attributes
|
|
196
|
+
* directly into the produced HTML — a single pass, not a second DOM walk
|
|
197
|
+
* after insertion.
|
|
198
|
+
*
|
|
199
|
+
* Fenced code blocks are syntax-highlighted via the same fine-grained `shiki/core` recipe
|
|
200
|
+
* `<lyra-code-block-core>` uses (`highlightCode`, default `true` — gated by whether a fenced
|
|
201
|
+
* block's language is a key in `languages`, since there is no default highlighter here to gate on
|
|
202
|
+
* "is shiki installed at all"). The very first render of any content is always plain (identical to
|
|
203
|
+
* `<lyra-markdown>`'s own output); highlighting arrives as an asynchronous upgrade one render
|
|
204
|
+
* later, once the fine-grained highlighter resolves. No highlighting is attempted while
|
|
205
|
+
* `streaming` is `true` — it applies once a stream settles, so there is no added per-chunk cost
|
|
206
|
+
* while content is still arriving.
|
|
207
|
+
*
|
|
208
|
+
* When `heading-anchors` is set, every rendered heading's slug (computed via the shared
|
|
209
|
+
* GitHub-slugger-style `Slugger`) is stamped as its `id`; `getHeadingTree()` computes that same
|
|
210
|
+
* outline on every parse regardless of `heading-anchors`, so a host can build a table of contents
|
|
211
|
+
* even while ids aren't in the DOM yet. `scrollToAnchor()` (from the adopted `DocumentAnchorTarget`
|
|
212
|
+
* mixin) resolves `fragment` anchors against that outline and `text-quote` anchors via
|
|
213
|
+
* `internal/text-quote.ts`'s shared scope/resolve helpers; `highlights` re-resolve by quote after
|
|
214
|
+
* every render (never by node identity), so a highlight painted before a `streaming` update
|
|
215
|
+
* finishes still finds its quote once the matching text arrives. Highlight painting uses
|
|
216
|
+
* `internal/text-highlights.ts`'s `acquireHighlightHandle()` -- the CSS Custom Highlight API where
|
|
217
|
+
* the browser supports it (no DOM mutation at all), a `<mark>`-wrap fallback otherwise.
|
|
218
|
+
*
|
|
219
|
+
* `math` renders `$...$`/`$$...$$` TeX as MathML via the optional `katex` peer's
|
|
220
|
+
* `renderToString(tex, { output: 'mathml' })` -- MathML Core renders natively and accessibly in
|
|
221
|
+
* evergreen browsers with no extra stylesheet or webfont needing to cross the shadow boundary. A
|
|
222
|
+
* missing `katex` peer renders the literal, unparsed TeX source (delimiters included) and fires one
|
|
223
|
+
* `lyra-render-error`.
|
|
224
|
+
*
|
|
225
|
+
* @customElement lyra-markdown-core
|
|
226
|
+
* @event lyra-link-click - Fired (and the click prevented) when a rendered
|
|
227
|
+
* link's `href` starts with `internal-link-prefix`. `detail: { href:
|
|
228
|
+
* string, internal: true }`. Ordinary external links navigate normally
|
|
229
|
+
* (in `link-target`) and never fire this event.
|
|
230
|
+
* @event lyra-render-error - Fired whenever rendering falls back to plain
|
|
231
|
+
* text, or `math` is set but the `katex` peer isn't installed. `detail: { error: unknown }`.
|
|
232
|
+
* @event lyra-highlight-activate - A painted `text-quote` highlight was clicked. `detail: { id }`.
|
|
233
|
+
* @event lyra-text-select - Fired on selection end inside the rendered content. `detail: { text,
|
|
234
|
+
* anchor, rects }`; `anchor` is a `text-quote` `LyraAnchor` scoped to the rendered content, or
|
|
235
|
+
* `null` if the selection couldn't be anchored.
|
|
236
|
+
* @event lyra-anchor-result - Fired after an `anchor` property assignment or a `scrollToAnchor()`
|
|
237
|
+
* call is applied. `detail: { found }`.
|
|
238
|
+
* @csspart content - The wrapper around the rendered (or plain-text
|
|
239
|
+
* fallback) output.
|
|
240
|
+
* @csspart heading - Every rendered `<h1>`–`<h6>` (shifted by
|
|
241
|
+
* `heading-offset`).
|
|
242
|
+
* @csspart paragraph - Every rendered `<p>`.
|
|
243
|
+
* @csspart list - Every rendered `<ul>`/`<ol>`.
|
|
244
|
+
* @csspart code-block - Every rendered fenced/indented `<pre>`.
|
|
245
|
+
* @csspart inline-code - Every rendered inline `<code>` span (backtick spans, not fenced blocks).
|
|
246
|
+
* @csspart link - Every rendered `<a>`.
|
|
247
|
+
* @csspart table - Every rendered `<table>`.
|
|
248
|
+
* @csspart blockquote - Every rendered `<blockquote>`.
|
|
249
|
+
* @csspart img - Every rendered `<img>`.
|
|
250
|
+
* @csspart math - A rendered inline or block math span (`data-display="inline"|"block"`).
|
|
251
|
+
*/
|
|
252
|
+
export class LyraMarkdownCore extends DocumentAnchorTarget(LyraMarkdownCoreBase) {
|
|
253
|
+
constructor() {
|
|
254
|
+
super(...arguments);
|
|
255
|
+
/** The Markdown source to render. */
|
|
256
|
+
this.content = '';
|
|
257
|
+
/** Sanitize marked's HTML output with DOMPurify before rendering. See the
|
|
258
|
+
* class doc for what happens when this is `true` (the default) but the
|
|
259
|
+
* `dompurify` peer isn't installed. */
|
|
260
|
+
this.sanitize = true;
|
|
261
|
+
/** When `true`, overrides marked's `html` renderer hook to emit the HTML-escaped source text
|
|
262
|
+
* instead of passing raw/sanitized markup through -- for a consumer rendering arbitrary
|
|
263
|
+
* already-written content (e.g. a historical chat/agent transcript full of code/XML/HTML
|
|
264
|
+
* snippets) where a stray angle bracket should render as visible text, not a real DOM element.
|
|
265
|
+
* Still lets GFM tables/lists/etc. render normally -- only raw embedded HTML is affected.
|
|
266
|
+
* `false` (the default) reproduces today's exact `marked`-default (sanitized-when-`sanitize`)
|
|
267
|
+
* passthrough behavior. */
|
|
268
|
+
this.escapeHtml = false;
|
|
269
|
+
/** Enable GitHub-flavored Markdown (tables, strikethrough, autolinks, task lists). */
|
|
270
|
+
this.gfm = true;
|
|
271
|
+
/** `target` applied to every rendered `<a>`, with `rel="noopener
|
|
272
|
+
* noreferrer"` always added alongside it whenever a `target` is emitted.
|
|
273
|
+
* `'_blank'` (the default) preserves today's exact output. Set to `null`
|
|
274
|
+
* (or the empty string, e.g. via the `link-target=""` attribute) to omit
|
|
275
|
+
* `target`/`rel` entirely, so rendered links open in the same tab. */
|
|
276
|
+
this.linkTarget = '_blank';
|
|
277
|
+
/** When set, a rendered link whose `href` starts with this prefix is
|
|
278
|
+
* treated as internal — its click is intercepted and reported via
|
|
279
|
+
* `lyra-link-click` instead of navigating. Empty (the default) means
|
|
280
|
+
* every link is treated as external. */
|
|
281
|
+
this.internalLinkPrefix = '';
|
|
282
|
+
/** Added to every rendered heading's source `token.depth` before emitting
|
|
283
|
+
* `<h${depth}>` — e.g. `heading-offset="2"` renders a source `#` as
|
|
284
|
+
* `<h3>` and a source `##` as `<h4>`. The result is clamped to `[1, 6]`
|
|
285
|
+
* (a source `######` with a positive offset stays at `<h6>` rather than
|
|
286
|
+
* overflowing past the HTML heading levels; the floor at `1` is
|
|
287
|
+
* defensive, since this property is meant to be additive-only). `0`
|
|
288
|
+
* (the default) preserves today's exact `<h${token.depth}>` output. */
|
|
289
|
+
this.headingOffset = 0;
|
|
290
|
+
/** When `true`, `connectedCallback()` skips awaiting `loadMarkdownDeps()`'s
|
|
291
|
+
* dynamic `import()` if the shared `marked`/`dompurify` module cache (see
|
|
292
|
+
* `markdown-loader.ts`'s `getMarkdownDepsIfLoaded()`) has *already*
|
|
293
|
+
* resolved — e.g. because an earlier `<lyra-markdown>` instance on the
|
|
294
|
+
* page already finished loading, or the consumer primed the cache
|
|
295
|
+
* directly by calling `loadMarkdownDeps()` themselves at startup — and
|
|
296
|
+
* renders synchronously instead. When the cache isn't warm yet (most
|
|
297
|
+
* notably: the very first `<lyra-markdown>` ever connected on a page,
|
|
298
|
+
* since nothing has called `loadMarkdownDeps()` before it), this still
|
|
299
|
+
* falls back to the normal async path — a dynamic `import()` can't be
|
|
300
|
+
* made synchronous, so this is a fast path for the common "already warm"
|
|
301
|
+
* case, not a hard guarantee. `false` (the default) is byte-identical to
|
|
302
|
+
* today: always the async `import()`, fallback-text window included. */
|
|
303
|
+
this.eagerLoad = false;
|
|
304
|
+
/** Signals that `content` is still arriving incrementally. Content changes
|
|
305
|
+
* continue to render immediately; while this is `true`, the host remains
|
|
306
|
+
* `aria-busy="true"` so assistive technology knows the rendered document
|
|
307
|
+
* is not final. Set it back to `false` with the final content update.
|
|
308
|
+
* Reflects so a consumer can also target `lyra-markdown[streaming]`. */
|
|
309
|
+
this.streaming = false;
|
|
310
|
+
/** Syntax-highlights fenced code blocks via the same optional `shiki` peer `<lyra-code-block>`
|
|
311
|
+
* uses. `true` (the default) upgrades every fenced block from plain `<pre><code>` once the peer
|
|
312
|
+
* is available -- a pure upgrade, not a behavior change gated on opt-in, since it's itself gated
|
|
313
|
+
* transparently by whether `shiki` is installed at all (an app that never installs it sees
|
|
314
|
+
* byte-identical output to today). Set `false` to keep plain output even when `shiki` is
|
|
315
|
+
* installed. No effect while `streaming` is `true` -- see that property's own doc. */
|
|
316
|
+
this.highlightCode = true;
|
|
317
|
+
/** Grammar definitions this instance can highlight, e.g. `{ json: jsonGrammar }` (import from
|
|
318
|
+
* `shiki/langs/<name>.mjs`), forwarded verbatim to `loadShikiHighlighterCore()` -- same shape as
|
|
319
|
+
* `<lyra-code-block-core>`'s own `languages`. This component has no default/full-table
|
|
320
|
+
* highlighter to fall back to -- a fenced block whose language isn't a key here always renders
|
|
321
|
+
* the plain-text fallback. Empty (the default) never highlights anything. */
|
|
322
|
+
this.languages = {};
|
|
323
|
+
/** Stamps a computed slug as `id` on every rendered heading. `getHeadingTree()` computes the
|
|
324
|
+
* same slugs regardless of this property -- it only controls whether the `id` attribute is
|
|
325
|
+
* emitted into the rendered DOM. `false` (the default) preserves today's exact output.
|
|
326
|
+
*
|
|
327
|
+
* When `sanitize` is also on (the default), a slug whose *value* collides with a real
|
|
328
|
+
* `document` property name (e.g. a heading literally titled "Title", "Location", or "Forms"
|
|
329
|
+
* slugs to `title`/`location`/`forms`) has its `id` silently stripped by DOMPurify's DOM-
|
|
330
|
+
* clobbering protection (`SANITIZE_DOM`) -- `getHeadingTree()` still reports that heading's slug
|
|
331
|
+
* either way, but `scrollToAnchor({ kind: 'fragment', id })` still resolves it correctly even
|
|
332
|
+
* without a DOM `id` present, via its own position-based fallback lookup. */
|
|
333
|
+
this.headingAnchors = false;
|
|
334
|
+
/** Renders `$...$`/`$$...$$` TeX via the optional `katex` peer, as MathML. `false` (the
|
|
335
|
+
* default) renders `$...$` literally, unparsed -- today's exact output. */
|
|
336
|
+
this.math = false;
|
|
337
|
+
/** Anchor kinds this component resolves via `scrollToAnchor()`. Readonly. */
|
|
338
|
+
this.anchorKinds = ['fragment', 'text-quote'];
|
|
339
|
+
// `null` covers both "the optional peers are still loading" and "a render
|
|
340
|
+
// attempt just fell back after a failure" — the two states intentionally
|
|
341
|
+
// look identical (plain text, see render()) since a consumer distinguishes
|
|
342
|
+
// them via `lyra-render-error`, not a visual difference.
|
|
343
|
+
this.renderedHtml = null;
|
|
344
|
+
/** Document-ordered heading outline computed on every parse (see `getHeadingTree()`), regardless
|
|
345
|
+
* of `headingAnchors`. */
|
|
346
|
+
this.headingTree = [];
|
|
347
|
+
/** The most recently resolved `text-quote` highlight ranges, kept for `onContentClick()`'s
|
|
348
|
+
* coordinate hit-test -- the CSS Custom Highlight API paints ranges without creating any DOM
|
|
349
|
+
* element to attach a click listener to, so activation is resolved by comparing the click point
|
|
350
|
+
* against each range's own `getClientRects()` instead, uniformly across both paint paths. */
|
|
351
|
+
this.resolvedHighlightRanges = [];
|
|
352
|
+
/** Guards `lyra-render-error` so a permanently-missing `katex` peer reports once per instance,
|
|
353
|
+
* not on every subsequent re-render while `math` stays on. Reset whenever `math` toggles. */
|
|
354
|
+
this.mathFailureReported = false;
|
|
355
|
+
/** `(lang, code)` -> already-highlighted HTML, content-addressed (see `PendingHighlight`'s doc).
|
|
356
|
+
* Persists across renders of this instance; populated asynchronously by `highlightPending()`.
|
|
357
|
+
* Never consulted while `streaming` is `true` or `highlightCode` is `false` -- both gates live
|
|
358
|
+
* in the `code()` renderer inside `parseMarkdown()`. Bounded to {@link HIGHLIGHT_CACHE_MAX}
|
|
359
|
+
* entries, least-recently-used first out, via `getCachedHighlight()`/`setCachedHighlight()` --
|
|
360
|
+
* always go through those instead of the map directly so hits refresh recency. */
|
|
361
|
+
this.highlightCache = new Map();
|
|
362
|
+
/** Bumped on every `highlightPending()` call, including ones that end up not actually loading
|
|
363
|
+
* anything -- guards against a newer `content`/`streaming` change superseding an older in-flight
|
|
364
|
+
* highlight, exactly mirroring `<lyra-code-block>`'s own `highlightToken` field for the identical
|
|
365
|
+
* race (an async grammar load resolving after a newer call already produced correct output). */
|
|
366
|
+
this.highlightToken = 0;
|
|
367
|
+
/** Keys from `PendingHighlight` that failed to highlight -- peer missing, language unrecognized,
|
|
368
|
+
* or tokenization threw. Once a key lands here, `code()` stops re-discovering it as pending on
|
|
369
|
+
* every future render. Without this, a permanently-unhighlightable block (e.g. an unrecognized
|
|
370
|
+
* language) would never get cached, so every `renderMarkdown()` pass -- including the one
|
|
371
|
+
* `highlightPending()` itself triggers on completion -- would rediscover it as pending and retry
|
|
372
|
+
* it again, forever. Mirrors `code-loader.ts`'s own `unsupportedLanguages` Set, which exists for
|
|
373
|
+
* the identical reason one level down (a single unrecognized `language` value on
|
|
374
|
+
* `<lyra-code-block>`). */
|
|
375
|
+
this.failedHighlightKeys = new Set();
|
|
376
|
+
// A single delegated listener on the content wrapper (not one per <a>) —
|
|
377
|
+
// the rendered markup is fully replaced on every content change, so a
|
|
378
|
+
// per-anchor listener would need re-attaching on every render anyway.
|
|
379
|
+
this.onContentClick = (e) => {
|
|
380
|
+
const highlightId = this.hitTestHighlightAt(e.clientX, e.clientY);
|
|
381
|
+
if (highlightId) {
|
|
382
|
+
this.emit('lyra-highlight-activate', { id: highlightId });
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const prefix = this.internalLinkPrefix;
|
|
386
|
+
if (!prefix)
|
|
387
|
+
return;
|
|
388
|
+
const anchor = e.composedPath().find((el) => el instanceof HTMLAnchorElement);
|
|
389
|
+
if (!anchor)
|
|
390
|
+
return;
|
|
391
|
+
// Compared against the raw `href` *attribute*, not the `.href` IDL
|
|
392
|
+
// property — the property is always browser-resolved to an absolute URL
|
|
393
|
+
// (e.g. "https://example.com/docs") even for a relative/prefixed path,
|
|
394
|
+
// which would never match a relative `internal-link-prefix`.
|
|
395
|
+
const href = anchor.getAttribute('href') ?? '';
|
|
396
|
+
if (!href.startsWith(prefix))
|
|
397
|
+
return;
|
|
398
|
+
e.preventDefault();
|
|
399
|
+
this.emit('lyra-link-click', { href, internal: true });
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
static { this.styles = [LyraElement.styles, styles, srOnly]; }
|
|
403
|
+
/** LRU read: a hit is re-inserted so Map iteration order (insertion order) keeps the first key
|
|
404
|
+
* the least recently used one -- the entry `setCachedHighlight()` evicts when full. */
|
|
405
|
+
getCachedHighlight(key) {
|
|
406
|
+
const cached = this.highlightCache.get(key);
|
|
407
|
+
if (cached !== undefined) {
|
|
408
|
+
this.highlightCache.delete(key);
|
|
409
|
+
this.highlightCache.set(key, cached);
|
|
410
|
+
}
|
|
411
|
+
return cached;
|
|
412
|
+
}
|
|
413
|
+
setCachedHighlight(key, html) {
|
|
414
|
+
if (this.highlightCache.has(key)) {
|
|
415
|
+
this.highlightCache.delete(key);
|
|
416
|
+
}
|
|
417
|
+
else if (this.highlightCache.size >= HIGHLIGHT_CACHE_MAX) {
|
|
418
|
+
const oldest = this.highlightCache.keys().next().value;
|
|
419
|
+
if (oldest !== undefined)
|
|
420
|
+
this.highlightCache.delete(oldest);
|
|
421
|
+
}
|
|
422
|
+
this.highlightCache.set(key, html);
|
|
423
|
+
}
|
|
424
|
+
connectedCallback() {
|
|
425
|
+
super.connectedCallback();
|
|
426
|
+
if (this.eagerLoad) {
|
|
427
|
+
// Only takes this synchronous path when the shared module cache has
|
|
428
|
+
// *already* settled (see getMarkdownDepsIfLoaded()'s doc) -- otherwise
|
|
429
|
+
// falls through to the same async path as the default below, since a
|
|
430
|
+
// dynamic import() can't be made synchronous from scratch.
|
|
431
|
+
const alreadyLoaded = getMarkdownDepsIfLoaded();
|
|
432
|
+
if (alreadyLoaded) {
|
|
433
|
+
this.deps = alreadyLoaded;
|
|
434
|
+
this.renderMarkdown();
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
void loadMarkdownDeps().then((resolved) => {
|
|
439
|
+
// The (module-cached, page-lifetime) loadMarkdownDeps() promise can
|
|
440
|
+
// resolve after this instance was removed from the DOM -- e.g. an
|
|
441
|
+
// <lyra-markdown> inside a conditionally-rendered chat message or a
|
|
442
|
+
// virtualized list. Without this guard, a detached instance would
|
|
443
|
+
// still have its `deps` set and renderMarkdown() called, mutating the
|
|
444
|
+
// @state() renderedHtml property and scheduling a Lit update no one
|
|
445
|
+
// will ever see. Mirrors chart.ts's own connectedCallback() guard for
|
|
446
|
+
// the identical race.
|
|
447
|
+
if (!this.isConnected)
|
|
448
|
+
return;
|
|
449
|
+
this.deps = resolved;
|
|
450
|
+
this.renderMarkdown();
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
disconnectedCallback() {
|
|
454
|
+
super.disconnectedCallback(); // reaches DocumentAnchorTarget's own cleanup (anchor retry, selection binding)
|
|
455
|
+
this.highlightHandle?.release();
|
|
456
|
+
this.highlightHandle = undefined;
|
|
457
|
+
}
|
|
458
|
+
/** Binds selection -> `lyra-text-select` once, on the stable `[part="content"]` wrapper --
|
|
459
|
+
* re-renders only replace that wrapper's children via `unsafeHTML`, never the wrapper itself.
|
|
460
|
+
* `bindTextSelection` is `protected` on the mixin's own narrowed return type (deliberately not
|
|
461
|
+
* part of `LyraAnchorTarget`'s public surface -- see `anchor-target.ts`'s class doc), so it's
|
|
462
|
+
* reached the same way that module's own tests do: through a cast, without declaring a no-op
|
|
463
|
+
* passthrough override just to satisfy the type checker. */
|
|
464
|
+
firstUpdated() {
|
|
465
|
+
const contentRoot = this.renderRoot.querySelector('[part="content"]');
|
|
466
|
+
if (!contentRoot)
|
|
467
|
+
return;
|
|
468
|
+
this.bindTextSelection(contentRoot);
|
|
469
|
+
}
|
|
470
|
+
// Runs before render (not updated()) so mutating the `renderedHtml` state
|
|
471
|
+
// property below is absorbed into the *same* update cycle instead of
|
|
472
|
+
// scheduling a second one -- Lit's documented pattern for deriving one
|
|
473
|
+
// reactive property from a change to others.
|
|
474
|
+
willUpdate(changed) {
|
|
475
|
+
super.willUpdate(changed); // reaches DocumentAnchorTarget's own willUpdate (declarative `anchor`)
|
|
476
|
+
if (changed.has('math'))
|
|
477
|
+
this.mathFailureReported = false;
|
|
478
|
+
if (!this.deps) {
|
|
479
|
+
// Still loading the optional peers — the connectedCallback promise
|
|
480
|
+
// above calls renderMarkdown() itself once they resolve, using
|
|
481
|
+
// whatever property values are current at that time.
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (changed.has('content') ||
|
|
485
|
+
changed.has('sanitize') ||
|
|
486
|
+
changed.has('gfm') ||
|
|
487
|
+
changed.has('linkTarget') ||
|
|
488
|
+
changed.has('headingOffset') ||
|
|
489
|
+
changed.has('escapeHtml') ||
|
|
490
|
+
changed.has('streaming') ||
|
|
491
|
+
changed.has('headingAnchors') ||
|
|
492
|
+
changed.has('math')) {
|
|
493
|
+
this.renderMarkdown();
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
updated(changed) {
|
|
497
|
+
super.updated(changed);
|
|
498
|
+
if (this.deps && !this.streaming) {
|
|
499
|
+
this.removeAttribute('aria-busy');
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
this.setAttribute('aria-busy', 'true');
|
|
503
|
+
}
|
|
504
|
+
if (changed.has('renderedHtml') || changed.has('highlights') || changed.has('activeHighlightId')) {
|
|
505
|
+
this.repaintHighlights();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
renderMarkdown() {
|
|
509
|
+
const deps = this.deps;
|
|
510
|
+
if (!deps)
|
|
511
|
+
return;
|
|
512
|
+
if (!deps.marked) {
|
|
513
|
+
// markdown-loader.ts already logged the specific import failure.
|
|
514
|
+
this.applyFallback(new Error('<lyra-markdown> could not render: the "marked" peer dependency failed to load.'));
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const pendingKeys = [];
|
|
518
|
+
const headingTree = [];
|
|
519
|
+
let rawHtml;
|
|
520
|
+
let hadMathFallback;
|
|
521
|
+
try {
|
|
522
|
+
const parsed = this.parseMarkdown(deps.marked, pendingKeys, headingTree);
|
|
523
|
+
rawHtml = parsed.html;
|
|
524
|
+
hadMathFallback = parsed.hadMathFallback;
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
this.applyFallback(error);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
this.headingTree = headingTree;
|
|
531
|
+
this.maybeLoadKatex();
|
|
532
|
+
// A math token rendering its literal fallback only means the `katex` peer is *confirmed*
|
|
533
|
+
// missing once loading has actually settled -- otherwise this is just the same one-microtask
|
|
534
|
+
// "still loading" window every other optional peer here has, and reporting it as an error
|
|
535
|
+
// would be a false positive.
|
|
536
|
+
const mathFailed = hadMathFallback && isKatexConfirmedMissing();
|
|
537
|
+
if (!this.sanitize) {
|
|
538
|
+
// Consumer explicitly opted out of sanitization — dompurify's
|
|
539
|
+
// presence or absence is irrelevant to this path.
|
|
540
|
+
this.renderedHtml = rawHtml;
|
|
541
|
+
if (mathFailed)
|
|
542
|
+
this.reportMathFailure();
|
|
543
|
+
this.maybeHighlightPending(pendingKeys);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (!deps.DOMPurify) {
|
|
547
|
+
const error = new Error('<lyra-markdown> could not render: sanitize is enabled (the default) but the "dompurify" peer ' +
|
|
548
|
+
'dependency failed to load — refusing to render unsanitized HTML. Install it with `pnpm add ' +
|
|
549
|
+
'dompurify`, or set sanitize="false" to explicitly opt out of sanitization.');
|
|
550
|
+
console.warn(error.message);
|
|
551
|
+
this.applyFallback(error);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
// `target` is not in DOMPurify's default attribute allowlist (unlike
|
|
555
|
+
// `part`/`rel`/`class`, which already are) — without ADD_ATTR here,
|
|
556
|
+
// every rendered link's target="..." would be silently stripped even
|
|
557
|
+
// though the anchor itself survives sanitization.
|
|
558
|
+
// 'style' joins 'target' in the added-attribute allowlist -- shiki's per-token output carries
|
|
559
|
+
// inline style="color:..." (theme colors), which isn't in DOMPurify's default attribute
|
|
560
|
+
// allowlist any more than 'target' was.
|
|
561
|
+
// `semantics`/`annotation` join the allowlist only when `math` is on -- the only KaTeX MathML
|
|
562
|
+
// output elements outside DOMPurify's default allowlist. `annotation-xml` is deliberately never
|
|
563
|
+
// added -- KaTeX's own MathML output never emits it, and DOMPurify's default allowlist already
|
|
564
|
+
// treats it as a namespace-switching element worth keeping stripped.
|
|
565
|
+
this.renderedHtml = deps.DOMPurify.sanitize(rawHtml, {
|
|
566
|
+
ADD_ATTR: ['target', 'style'],
|
|
567
|
+
...(this.math ? { ADD_TAGS: ['semantics', 'annotation'] } : {}),
|
|
568
|
+
});
|
|
569
|
+
if (mathFailed)
|
|
570
|
+
this.reportMathFailure();
|
|
571
|
+
this.maybeHighlightPending(pendingKeys);
|
|
572
|
+
}
|
|
573
|
+
applyFallback(error) {
|
|
574
|
+
this.renderedHtml = null;
|
|
575
|
+
this.emit('lyra-render-error', { error });
|
|
576
|
+
}
|
|
577
|
+
/** Kicks off the shared `katex` load the first time `math` needs it and no attempt is already
|
|
578
|
+
* in flight (module-scoped, so every `<lyra-markdown>` instance on the page shares one load --
|
|
579
|
+
* mirrors `markdown-loader.ts`'s own warm-cache shape). Skipped entirely under
|
|
580
|
+
* `__setKatexForTesting()` -- that seam controls math-rendering behavior directly and must never
|
|
581
|
+
* race a real, unmocked `import('katex')` settling underneath it. */
|
|
582
|
+
maybeLoadKatex() {
|
|
583
|
+
if (!this.math || katexLoadStarted || katexOverride !== undefined)
|
|
584
|
+
return;
|
|
585
|
+
katexLoadStarted = true;
|
|
586
|
+
void getKatex().then((katex) => {
|
|
587
|
+
resolvedKatex = katex;
|
|
588
|
+
if (!this.isConnected)
|
|
589
|
+
return;
|
|
590
|
+
this.renderMarkdown();
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
/** Fires `lyra-render-error` once per instance for a permanently-missing `katex` peer. Called
|
|
594
|
+
* only once `renderMarkdown()` has confirmed the peer is actually missing (`katexOverride` or
|
|
595
|
+
* the resolved module is `null`) -- a math token rendering its literal fallback while the load
|
|
596
|
+
* is merely still in flight (the same one-microtask transient window every other optional peer
|
|
597
|
+
* in this component has) never reports an error on its own. */
|
|
598
|
+
reportMathFailure() {
|
|
599
|
+
if (this.mathFailureReported)
|
|
600
|
+
return;
|
|
601
|
+
this.mathFailureReported = true;
|
|
602
|
+
this.emit('lyra-render-error', {
|
|
603
|
+
error: new Error('<lyra-markdown> needs the optional peer dependency `katex` to render math (the `math` property is set) — install it with `pnpm add katex`.'),
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
/** Kicks off async highlighting for `pendingKeys` (see `highlightPending()`) unless there's
|
|
607
|
+
* nothing to do, `highlightCode` is off, or `streaming` is on -- called from both of
|
|
608
|
+
* `renderMarkdown()`'s exit points (the `sanitize=false` early return and the normal sanitized
|
|
609
|
+
* path), since highlighting is independent of that decision. */
|
|
610
|
+
maybeHighlightPending(pendingKeys) {
|
|
611
|
+
if (pendingKeys.length === 0 || !this.highlightCode || this.streaming)
|
|
612
|
+
return;
|
|
613
|
+
void this.highlightPending(pendingKeys);
|
|
614
|
+
}
|
|
615
|
+
/** Loads whatever shiki grammars `pendingKeys` need, tokenizes each pending block concurrently,
|
|
616
|
+
* populates `highlightCache` with the results, then triggers one more `renderMarkdown()` pass so
|
|
617
|
+
* the newly-cached entries actually reach the screen. A pending key whose language isn't a key in
|
|
618
|
+
* `languages`, or otherwise fails to tokenize, is recorded in `failedHighlightKeys` -- it stays
|
|
619
|
+
* uncached, so `code()` keeps emitting its plain fallback for it on every future render, and
|
|
620
|
+
* `code()` also stops re-discovering it as pending (see `failedHighlightKeys`'s own doc for why
|
|
621
|
+
* that matters: without it, the `renderMarkdown()` call at the end of this method would
|
|
622
|
+
* rediscover the same permanently-uncacheable key as pending on every pass, forever). Does not
|
|
623
|
+
* block or delay any other pending key -- each is tried independently via `Promise.all`. */
|
|
624
|
+
async highlightPending(pendingKeys) {
|
|
625
|
+
const token = ++this.highlightToken;
|
|
626
|
+
const languages = this.languages;
|
|
627
|
+
const tokenizeOne = async (pending) => {
|
|
628
|
+
const normalizedLang = normalizeShikiLanguage(pending.lang);
|
|
629
|
+
if (!languages[normalizedLang] && !languages[pending.lang]) {
|
|
630
|
+
// Not in the supplied languages map -- there is no default/full-table highlighter to fall
|
|
631
|
+
// back to in this variant (mirrors <lyra-code-block-core>). Permanent for this key:
|
|
632
|
+
// failedHighlightKeys is never cleared, and languages isn't in willUpdate()'s trigger list,
|
|
633
|
+
// so changing it doesn't retry this key on its own.
|
|
634
|
+
this.failedHighlightKeys.add(pending.key);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const hl = await loadShikiHighlighterCore(languages);
|
|
638
|
+
if (!hl) {
|
|
639
|
+
this.failedHighlightKeys.add(pending.key);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
643
|
+
const html = hl.codeToHtml(pending.code, {
|
|
644
|
+
lang: normalizedLang,
|
|
645
|
+
themes: SHIKI_THEMES,
|
|
646
|
+
transformers: [markdownCodeTransformer(pending.lang)],
|
|
647
|
+
});
|
|
648
|
+
this.setCachedHighlight(pending.key, `${html}\n`);
|
|
649
|
+
}
|
|
650
|
+
catch {
|
|
651
|
+
this.failedHighlightKeys.add(pending.key);
|
|
652
|
+
// Tokenization failed for a reason other than an unrecognized grammar -- leave this key
|
|
653
|
+
// uncached, same effect as an unrecognized language: permanent plain fallback for this
|
|
654
|
+
// specific block.
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
await Promise.all(pendingKeys.map(tokenizeOne));
|
|
658
|
+
if (token !== this.highlightToken || !this.isConnected)
|
|
659
|
+
return;
|
|
660
|
+
this.renderMarkdown();
|
|
661
|
+
}
|
|
662
|
+
parseMarkdown(marked, pendingKeys, headingTreeOut) {
|
|
663
|
+
// Falsy (`null` or `''`) means the consumer explicitly opted out of
|
|
664
|
+
// target="..."/rel="..." on rendered links -- see the linkTarget doc.
|
|
665
|
+
// The default '_blank' is already truthy, so this preserves today's
|
|
666
|
+
// exact output when the property is left unset.
|
|
667
|
+
const linkTarget = this.linkTarget;
|
|
668
|
+
// A raw NaN (e.g. an invalid `heading-offset` attribute) would otherwise flow straight into
|
|
669
|
+
// `token.depth + headingOffset` below, producing a NaN heading depth and an invalid `<hNaN>`
|
|
670
|
+
// tag -- finiteInteger() normalizes it back to the documented `0` (additive-only) default.
|
|
671
|
+
const headingOffset = finiteInteger(this.headingOffset, 0);
|
|
672
|
+
// Captured as a local distinct from the free function `escapeHtml` above
|
|
673
|
+
// (imported/defined at the top of this file) to avoid shadowing/
|
|
674
|
+
// ambiguity inside the renderer closure below, matching how
|
|
675
|
+
// `linkTarget`/`headingOffset` are already captured as locals for the
|
|
676
|
+
// same reason.
|
|
677
|
+
const escapeHtmlOption = this.escapeHtml;
|
|
678
|
+
const highlightCodeOption = this.highlightCode && !this.streaming;
|
|
679
|
+
// Bound method reference (not the raw map): reads must go through the LRU accessor so a hit
|
|
680
|
+
// refreshes its recency. The renderer methods below run with marked's own `this`.
|
|
681
|
+
const getCachedHighlight = (key) => this.getCachedHighlight(key);
|
|
682
|
+
const failedHighlightKeys = this.failedHighlightKeys;
|
|
683
|
+
const headingAnchorsOption = this.headingAnchors;
|
|
684
|
+
const slugger = new Slugger();
|
|
685
|
+
const mathOption = this.math;
|
|
686
|
+
const cachedKatex = mathOption ? getKatexIfLoaded() : null;
|
|
687
|
+
let hadMathFallback = false;
|
|
688
|
+
const instance = new marked.Marked();
|
|
689
|
+
if (mathOption) {
|
|
690
|
+
instance.use({
|
|
691
|
+
extensions: [
|
|
692
|
+
mathExtension((token) => {
|
|
693
|
+
if (!cachedKatex) {
|
|
694
|
+
hadMathFallback = true;
|
|
695
|
+
return escapeHtml(token.display ? `$$${token.tex}$$` : `$${token.tex}$`);
|
|
696
|
+
}
|
|
697
|
+
try {
|
|
698
|
+
const mathml = cachedKatex.renderToString(token.tex, { output: 'mathml', throwOnError: false });
|
|
699
|
+
return `<span part="math" data-display="${token.display ? 'block' : 'inline'}">${mathml}</span>`;
|
|
700
|
+
}
|
|
701
|
+
catch {
|
|
702
|
+
// throwOnError: false already handles a malformed-TeX render internally (KaTeX's own
|
|
703
|
+
// inline error form); this catch only guards against an unexpected non-KaTeX failure,
|
|
704
|
+
// e.g. a broken/incompatible peer version -- same literal fallback, no event fired.
|
|
705
|
+
return escapeHtml(token.display ? `$$${token.tex}$$` : `$${token.tex}$`);
|
|
706
|
+
}
|
|
707
|
+
}),
|
|
708
|
+
],
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
// A fresh renderer per parse (rather than a shared/cached one) so it
|
|
712
|
+
// always closes over the *current* `linkTarget`/`headingOffset` — these
|
|
713
|
+
// properties can change between renders, and marked's `.use()` otherwise
|
|
714
|
+
// persists whatever renderer it was given for the lifetime of the
|
|
715
|
+
// instance.
|
|
716
|
+
instance.use({
|
|
717
|
+
renderer: {
|
|
718
|
+
heading(token) {
|
|
719
|
+
// Clamped to [1, 6]: a positive offset can never push a heading
|
|
720
|
+
// past <h6> (there is no <h7>), and the floor at 1 is defensive
|
|
721
|
+
// since headingOffset is meant to be additive-only (0 is the only
|
|
722
|
+
// documented non-positive value).
|
|
723
|
+
const depth = Math.min(6, Math.max(1, token.depth + headingOffset));
|
|
724
|
+
// Rendered through the plain textRenderer so markup never leaks into the slug -- an
|
|
725
|
+
// inline <code>/<em> inside a heading collapses to plain text for slugging purposes.
|
|
726
|
+
const label = this.parser.parseInline(token.tokens, this.parser.textRenderer);
|
|
727
|
+
const slug = slugger.slug(label);
|
|
728
|
+
headingTreeOut.push({ id: slug, label, level: depth });
|
|
729
|
+
const idAttr = headingAnchorsOption && slug ? ` id="${escapeHtml(slug)}"` : '';
|
|
730
|
+
return `<h${depth} part="heading"${idAttr}>${this.parser.parseInline(token.tokens)}</h${depth}>\n`;
|
|
731
|
+
},
|
|
732
|
+
paragraph(token) {
|
|
733
|
+
return `<p part="paragraph">${this.parser.parseInline(token.tokens)}</p>\n`;
|
|
734
|
+
},
|
|
735
|
+
list(token) {
|
|
736
|
+
const ordered = token.ordered;
|
|
737
|
+
const start = token.start;
|
|
738
|
+
let body = '';
|
|
739
|
+
for (const item of token.items)
|
|
740
|
+
body += this.listitem(item);
|
|
741
|
+
const tag = ordered ? 'ol' : 'ul';
|
|
742
|
+
const startAttr = ordered && start !== 1 ? ` start="${start}"` : '';
|
|
743
|
+
return `<${tag} part="list"${startAttr}>\n${body}</${tag}>\n`;
|
|
744
|
+
},
|
|
745
|
+
code(token) {
|
|
746
|
+
const lang = (token.lang ?? '').trim().split(/\s+/)[0] ?? '';
|
|
747
|
+
const body = `${token.text.replace(/\n$/, '')}\n`;
|
|
748
|
+
const text = token.escaped ? body : escapeHtml(body);
|
|
749
|
+
if (highlightCodeOption && lang) {
|
|
750
|
+
const key = `${lang}\n${body}`;
|
|
751
|
+
const cached = getCachedHighlight(key);
|
|
752
|
+
if (cached !== undefined)
|
|
753
|
+
return cached;
|
|
754
|
+
if (!failedHighlightKeys.has(key))
|
|
755
|
+
pendingKeys.push({ key, lang, code: body });
|
|
756
|
+
}
|
|
757
|
+
const cls = lang ? ` class="language-${escapeHtml(lang)}"` : '';
|
|
758
|
+
return `<pre part="code-block"><code${cls}>${text}</code></pre>\n`;
|
|
759
|
+
},
|
|
760
|
+
codespan(token) {
|
|
761
|
+
// Mirrors marked's own default codespan() renderer's escaping exactly (it does not
|
|
762
|
+
// pre-escape token.text itself) -- only the added part="inline-code" differs.
|
|
763
|
+
return `<code part="inline-code">${escapeHtml(token.text)}</code>`;
|
|
764
|
+
},
|
|
765
|
+
blockquote(token) {
|
|
766
|
+
return `<blockquote part="blockquote">\n${this.parser.parse(token.tokens)}</blockquote>\n`;
|
|
767
|
+
},
|
|
768
|
+
table(token) {
|
|
769
|
+
// Built directly here (rather than delegating to the inherited
|
|
770
|
+
// tablecell()) so a scope="col" can be added -- marked's own
|
|
771
|
+
// default tablecell() never emits it, and without it a screen
|
|
772
|
+
// reader can't reliably associate a data cell with its column
|
|
773
|
+
// header beyond the simplest table.
|
|
774
|
+
let headerRow = '';
|
|
775
|
+
for (const cell of token.header) {
|
|
776
|
+
const text = this.parser.parseInline(cell.tokens);
|
|
777
|
+
const alignAttr = cell.align ? ` align="${cell.align}"` : '';
|
|
778
|
+
headerRow += `<th scope="col"${alignAttr}>${text}</th>`;
|
|
779
|
+
}
|
|
780
|
+
let bodyRows = '';
|
|
781
|
+
for (const row of token.rows) {
|
|
782
|
+
let rowHtml = '';
|
|
783
|
+
for (const cell of row)
|
|
784
|
+
rowHtml += this.tablecell(cell);
|
|
785
|
+
bodyRows += this.tablerow({ text: rowHtml });
|
|
786
|
+
}
|
|
787
|
+
const thead = `<thead>\n${this.tablerow({ text: headerRow })}</thead>\n`;
|
|
788
|
+
const tbody = bodyRows ? `<tbody>${bodyRows}</tbody>\n` : '';
|
|
789
|
+
return `<table part="table">\n${thead}${tbody}</table>\n`;
|
|
790
|
+
},
|
|
791
|
+
link(token) {
|
|
792
|
+
const text = this.parser.parseInline(token.tokens);
|
|
793
|
+
const href = cleanHref(token.href);
|
|
794
|
+
if (href === null)
|
|
795
|
+
return text;
|
|
796
|
+
const titleAttr = token.title ? ` title="${escapeHtml(token.title)}"` : '';
|
|
797
|
+
const targetAttr = linkTarget
|
|
798
|
+
? ` target="${escapeHtml(linkTarget)}" rel="noopener noreferrer"`
|
|
799
|
+
: '';
|
|
800
|
+
return `<a part="link" href="${escapeHtml(href)}"${titleAttr}${targetAttr}>${text}</a>`;
|
|
801
|
+
},
|
|
802
|
+
image(token) {
|
|
803
|
+
// Mirrors marked's own default image() renderer (alt text
|
|
804
|
+
// re-rendered through the plain textRenderer so nested emphasis/
|
|
805
|
+
// strong/etc. inside the alt collapses to plain text, href run
|
|
806
|
+
// through the same cleanHref() the link() override above uses)
|
|
807
|
+
// with a part="img" added.
|
|
808
|
+
const altText = this.parser.parseInline(token.tokens, this.parser.textRenderer);
|
|
809
|
+
const href = cleanHref(token.href);
|
|
810
|
+
if (href === null)
|
|
811
|
+
return escapeHtml(altText);
|
|
812
|
+
const titleAttr = token.title ? ` title="${escapeHtml(token.title)}"` : '';
|
|
813
|
+
return `<img part="img" src="${escapeHtml(href)}" alt="${escapeHtml(altText)}"${titleAttr}>`;
|
|
814
|
+
},
|
|
815
|
+
html(token) {
|
|
816
|
+
return escapeHtmlOption ? escapeHtml(token.text) : token.text;
|
|
817
|
+
},
|
|
818
|
+
},
|
|
819
|
+
});
|
|
820
|
+
return { html: instance.parse(this.content, { gfm: this.gfm, async: false }), hadMathFallback };
|
|
821
|
+
}
|
|
822
|
+
/** A document-ordered, flattened heading outline -- computed on every parse regardless of
|
|
823
|
+
* `headingAnchors` (see that property's own doc). A caller building a table of contents can rely
|
|
824
|
+
* on this even while `heading-anchors` is off. */
|
|
825
|
+
getHeadingTree() {
|
|
826
|
+
return [...this.headingTree];
|
|
827
|
+
}
|
|
828
|
+
contentRoot() {
|
|
829
|
+
return this.renderRoot.querySelector('[part="content"]');
|
|
830
|
+
}
|
|
831
|
+
// -- anchor-target: applyAnchor per kind -----------------------------------------------------
|
|
832
|
+
async applyAnchor(anchor) {
|
|
833
|
+
const root = this.contentRoot();
|
|
834
|
+
if (!root)
|
|
835
|
+
return false;
|
|
836
|
+
switch (anchor.kind) {
|
|
837
|
+
case 'fragment':
|
|
838
|
+
return this.applyFragmentAnchor(root, anchor);
|
|
839
|
+
case 'text-quote':
|
|
840
|
+
return this.applyTextQuoteAnchor(root, anchor);
|
|
841
|
+
default:
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
applyFragmentAnchor(root, anchor) {
|
|
846
|
+
if (!anchor.id)
|
|
847
|
+
return false;
|
|
848
|
+
const known = this.headingTree.some((h) => h.id === anchor.id);
|
|
849
|
+
if (!known)
|
|
850
|
+
return false;
|
|
851
|
+
const el = root.querySelector(`#${CSS.escape(anchor.id)}`) ?? this.findHeadingByComputedId(root, anchor.id);
|
|
852
|
+
if (!el)
|
|
853
|
+
return false;
|
|
854
|
+
el.scrollIntoView({ behavior: prefersReducedMotion() ? 'auto' : 'smooth', block: 'start' });
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
/** `headingAnchors` may be off, so a target heading might carry no `id` attribute in the DOM --
|
|
858
|
+
* re-derives the same slug order `getHeadingTree()` was built in and matches by position
|
|
859
|
+
* instead of by attribute. */
|
|
860
|
+
findHeadingByComputedId(root, id) {
|
|
861
|
+
const index = this.headingTree.findIndex((h) => h.id === id);
|
|
862
|
+
if (index < 0)
|
|
863
|
+
return null;
|
|
864
|
+
const headings = root.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
|
865
|
+
return headings[index] ?? null;
|
|
866
|
+
}
|
|
867
|
+
applyTextQuoteAnchor(root, anchor) {
|
|
868
|
+
const range = resolveTextQuote(scopeFromElement(root), anchor);
|
|
869
|
+
if (!range)
|
|
870
|
+
return false;
|
|
871
|
+
const target = range.startContainer.nodeType === Node.ELEMENT_NODE ? range.startContainer : range.startContainer.parentElement;
|
|
872
|
+
(target ?? root).scrollIntoView({ behavior: prefersReducedMotion() ? 'auto' : 'smooth', block: 'center' });
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
/** Overrides `DocumentAnchorTarget`'s default (whole render-root) selection scope, matching
|
|
876
|
+
* `heading`'s slug computation and `applyTextQuoteAnchor()` above: only `[part="content"]` is a
|
|
877
|
+
* meaningful text-quote scope, so a selection that somehow reaches outside it (there is no other
|
|
878
|
+
* text in this component's shadow tree today, but the live region could grow one) never leaks
|
|
879
|
+
* into a captured anchor. */
|
|
880
|
+
computeSelectionAnchor(range) {
|
|
881
|
+
const root = this.contentRoot();
|
|
882
|
+
if (!root)
|
|
883
|
+
return null;
|
|
884
|
+
return buildQuoteAnchor(range, scopeFromElement(root));
|
|
885
|
+
}
|
|
886
|
+
// -- highlight painting ------------------------------------------------------------------------
|
|
887
|
+
ensureHighlightHandle() {
|
|
888
|
+
if (!this.highlightHandle)
|
|
889
|
+
this.highlightHandle = acquireHighlightHandle(this, this.ownerDocument);
|
|
890
|
+
return this.highlightHandle;
|
|
891
|
+
}
|
|
892
|
+
/** Re-resolves every `text-quote` highlight against the current rendered content and repaints
|
|
893
|
+
* via `acquireHighlightHandle()` -- resolution is always by quote text, never by node identity,
|
|
894
|
+
* so a highlight set before its quote exists in `content` yet (e.g. mid-`streaming`) simply
|
|
895
|
+
* paints nothing until a later render's text actually contains it. `fragment` highlights aren't
|
|
896
|
+
* painted (there is no literal span of text to wrap/underline for a whole section). */
|
|
897
|
+
repaintHighlights() {
|
|
898
|
+
this.resolvedHighlightRanges = [];
|
|
899
|
+
const root = this.contentRoot();
|
|
900
|
+
if (!root)
|
|
901
|
+
return;
|
|
902
|
+
const handle = this.ensureHighlightHandle();
|
|
903
|
+
const scope = scopeFromElement(root);
|
|
904
|
+
const rangesByTone = new Map(HIGHLIGHT_TONES.map((tone) => [tone, []]));
|
|
905
|
+
let activeRange = null;
|
|
906
|
+
for (const highlight of this.highlights) {
|
|
907
|
+
if (highlight.anchor.kind !== 'text-quote')
|
|
908
|
+
continue;
|
|
909
|
+
const range = resolveTextQuote(scope, highlight.anchor);
|
|
910
|
+
if (!range)
|
|
911
|
+
continue;
|
|
912
|
+
rangesByTone.get(highlight.tone ?? 'accent').push(range);
|
|
913
|
+
this.resolvedHighlightRanges.push({ id: highlight.id, range });
|
|
914
|
+
if (highlight.id === this.activeHighlightId)
|
|
915
|
+
activeRange = range;
|
|
916
|
+
}
|
|
917
|
+
for (const [tone, ranges] of rangesByTone)
|
|
918
|
+
handle.setRanges(tone, ranges);
|
|
919
|
+
handle.setActive(activeRange);
|
|
920
|
+
if (!supportsCustomHighlights()) {
|
|
921
|
+
// The `<mark>`-wrap fallback creates real elements but carries no `part` of its own (the
|
|
922
|
+
// module is shared by every adopting viewer, so it can't know this component's part naming)
|
|
923
|
+
// -- stamped here so a consumer can still target `::part(highlight)` in browsers lacking the
|
|
924
|
+
// CSS Custom Highlight API. Nothing to stamp on the API path: no DOM element is created there.
|
|
925
|
+
for (const mark of root.querySelectorAll('mark[data-lyra-highlight-tone]')) {
|
|
926
|
+
if (!mark.hasAttribute('part'))
|
|
927
|
+
mark.setAttribute('part', 'highlight');
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
/** Hit-tests a click point against every currently-resolved highlight's `getClientRects()`,
|
|
932
|
+
* topmost (last-resolved) first. The CSS Custom Highlight API paints ranges without creating any
|
|
933
|
+
* DOM element to attach a click listener to, so this is the only activation path that works
|
|
934
|
+
* identically on both paint paths -- mirrors `<lyra-pdf-viewer>`'s own coordinate-based
|
|
935
|
+
* `onPageClick()` hit-test for the same reason (its own painted highlights sit under a text
|
|
936
|
+
* layer that intercepts most pointer events). */
|
|
937
|
+
hitTestHighlightAt(x, y) {
|
|
938
|
+
for (let i = this.resolvedHighlightRanges.length - 1; i >= 0; i--) {
|
|
939
|
+
const { id, range } = this.resolvedHighlightRanges[i];
|
|
940
|
+
for (const rect of range.getClientRects()) {
|
|
941
|
+
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom)
|
|
942
|
+
return id;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return null;
|
|
946
|
+
}
|
|
947
|
+
render() {
|
|
948
|
+
const isFallback = this.renderedHtml === null;
|
|
949
|
+
return html `
|
|
950
|
+
<div part="content" ?data-fallback=${isFallback} @click=${this.onContentClick}>
|
|
951
|
+
${isFallback ? this.content : unsafeHTML(this.renderedHtml)}
|
|
952
|
+
</div>
|
|
953
|
+
${this.renderAnchorLiveRegion()}
|
|
954
|
+
`;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
__decorate([
|
|
958
|
+
property()
|
|
959
|
+
], LyraMarkdownCore.prototype, "content", void 0);
|
|
960
|
+
__decorate([
|
|
961
|
+
property({ type: Boolean })
|
|
962
|
+
], LyraMarkdownCore.prototype, "sanitize", void 0);
|
|
963
|
+
__decorate([
|
|
964
|
+
property({ type: Boolean, attribute: 'escape-html' })
|
|
965
|
+
], LyraMarkdownCore.prototype, "escapeHtml", void 0);
|
|
966
|
+
__decorate([
|
|
967
|
+
property({ type: Boolean })
|
|
968
|
+
], LyraMarkdownCore.prototype, "gfm", void 0);
|
|
969
|
+
__decorate([
|
|
970
|
+
property({ attribute: 'link-target' })
|
|
971
|
+
], LyraMarkdownCore.prototype, "linkTarget", void 0);
|
|
972
|
+
__decorate([
|
|
973
|
+
property({ attribute: 'internal-link-prefix' })
|
|
974
|
+
], LyraMarkdownCore.prototype, "internalLinkPrefix", void 0);
|
|
975
|
+
__decorate([
|
|
976
|
+
property({ type: Number, attribute: 'heading-offset' })
|
|
977
|
+
], LyraMarkdownCore.prototype, "headingOffset", void 0);
|
|
978
|
+
__decorate([
|
|
979
|
+
property({ type: Boolean, attribute: 'eager-load' })
|
|
980
|
+
], LyraMarkdownCore.prototype, "eagerLoad", void 0);
|
|
981
|
+
__decorate([
|
|
982
|
+
property({ type: Boolean, reflect: true })
|
|
983
|
+
], LyraMarkdownCore.prototype, "streaming", void 0);
|
|
984
|
+
__decorate([
|
|
985
|
+
property({ type: Boolean, attribute: 'highlight-code' })
|
|
986
|
+
], LyraMarkdownCore.prototype, "highlightCode", void 0);
|
|
987
|
+
__decorate([
|
|
988
|
+
property({ attribute: false })
|
|
989
|
+
], LyraMarkdownCore.prototype, "languages", void 0);
|
|
990
|
+
__decorate([
|
|
991
|
+
property({ type: Boolean, attribute: 'heading-anchors' })
|
|
992
|
+
], LyraMarkdownCore.prototype, "headingAnchors", void 0);
|
|
993
|
+
__decorate([
|
|
994
|
+
property({ type: Boolean })
|
|
995
|
+
], LyraMarkdownCore.prototype, "math", void 0);
|
|
996
|
+
__decorate([
|
|
997
|
+
state()
|
|
998
|
+
], LyraMarkdownCore.prototype, "renderedHtml", void 0);
|
|
999
|
+
//# sourceMappingURL=markdown-core.class.js.map
|