@c2n/code-viewer 0.0.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Nguyen Thai Vinh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,26 @@
1
+ # @c2n/code-viewer
2
+
3
+ Syntax-highlighted code blocks powered by [shiki](https://shiki.style), built with Lit.
4
+
5
+ ```bash
6
+ npm install @c2n/code-viewer
7
+ ```
8
+
9
+ ```html
10
+ <script type="module">
11
+ import '@c2n/code-viewer'
12
+ </script>
13
+
14
+ <c2-code-viewer language="ts" line-numbers highlight-lines="2" copyable>
15
+ <span slot="title">greet.ts</span>
16
+ export function greet(name: string) { return `Hello, ${name}!` }
17
+ </c2-code-viewer>
18
+ ```
19
+
20
+ - **Source**: the default slot, with text in a `<pre>` (formatters collapse whitespace inside custom elements but leave `<pre>` alone) or markup in a `<template>` (so the browser does not parse it); indentation is stripped. Or the `code` property / attribute, where `\n` stands for a newline.
21
+ - **Languages and themes**: `language` takes any shiki id or alias; `theme` any built-in theme name or `css-variables`. Grammars and themes load on demand into one shared highlighter using shiki's JavaScript engine, no WASM.
22
+ - **Dark mode**: add `dark-theme` and both palettes are rendered; `color-scheme="auto|light|dark"` picks one (auto follows `prefers-color-scheme`).
23
+ - **Extras**: `line-numbers` (+ `start-line`), `highlight-lines="2-4,7"`, `copyable` (fires `code-copy`), `wrap`, `inline`, a `title` slot for a file name header, and `--c2-code-viewer--max-height` for scrolling.
24
+ - **Progressive**: the plain code renders immediately (server-side too) and is upgraded in place once highlighted.
25
+
26
+ Theming: `--c2-code-viewer--*` for the frame (font, padding, borders, radius, max-height; `background` and `color` override the theme), `__header`, `__copy`, `__line-numbers` and `__line__highlighted` for the parts, and `__theme--*` for the colours of the `css-variables` theme. The full list is in `custom-elements.json` and on the docs site.
@@ -0,0 +1,350 @@
1
+ import { LitElement, html, nothing, unsafeCSS } from "lit";
2
+ import { customElement, property, state } from "lit/decorators.js";
3
+ import { classMap } from "lit/directives/class-map.js";
4
+ import { unsafeHTML } from "lit/directives/unsafe-html.js";
5
+ import { Task } from "@lit/task";
6
+ import { createCssVariablesTheme, createHighlighterCore } from "shiki/core";
7
+ import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
8
+ import { bundledLanguages } from "shiki/langs";
9
+ import { bundledThemes } from "shiki/themes";
10
+ //#region src/code-viewer.scss?inline
11
+ var code_viewer_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: block;\n min-width: 0;\n}\n\n:host([inline]) {\n display: inline;\n}\n\n.c2-code-viewer {\n display: flex;\n flex-direction: column;\n min-width: 0;\n overflow: clip;\n position: relative;\n background: var(--c2-code-viewer--background,var(--cv-bg,var(--shiki-light-bg)));\n color: var(--c2-code-viewer--color,var(--cv-fg,var(--shiki-light)));\n box-shadow: var(--c2-code-viewer--box-shadow);\n border-top: var(--c2-code-viewer--border-top,1px solid rgb(213, 213, 213));\n border-right: var(--c2-code-viewer--border-right,1px solid rgb(213, 213, 213));\n border-bottom: var(--c2-code-viewer--border-bottom,1px solid rgb(213, 213, 213));\n border-left: var(--c2-code-viewer--border-left,1px solid rgb(213, 213, 213));\n border-top-left-radius: var(--c2-code-viewer--border-top-left-radius,8px);\n border-top-right-radius: var(--c2-code-viewer--border-top-right-radius,8px);\n border-bottom-left-radius: var(--c2-code-viewer--border-bottom-left-radius,8px);\n border-bottom-right-radius: var(--c2-code-viewer--border-bottom-right-radius,8px);\n font-family: var(--c2-code-viewer--font-family,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);\n font-size: var(--c2-code-viewer--font-size,13px);\n line-height: var(--c2-code-viewer--line-height,1.6);\n}\n\n.c2-code-viewer-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding-top: var(--c2-code-viewer__header--padding-top,6px);\n padding-right: var(--c2-code-viewer__header--padding-right,8px);\n padding-bottom: var(--c2-code-viewer__header--padding-bottom,6px);\n padding-left: var(--c2-code-viewer__header--padding-left,16px);\n background: var(--c2-code-viewer__header--background,rgba(127, 127, 127, 0.08));\n color: var(--c2-code-viewer__header--color);\n font-family: var(--c2-code-viewer__header--font-family,var(--c2-code-viewer--font-family,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace));\n font-size: var(--c2-code-viewer__header--font-size,12px);\n border-bottom: var(--c2-code-viewer__header--border-bottom,1px solid rgba(127, 127, 127, 0.2));\n}\n.c2-code-viewer-header slot[name=title] {\n display: block;\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.c2-code-viewer-header[hidden] {\n display: none;\n}\n\n.c2-code-viewer-body {\n position: relative;\n min-width: 0;\n max-height: var(--c2-code-viewer--max-height);\n overflow: auto;\n}\n.c2-code-viewer-body > .c2-code-viewer-copy {\n position: absolute;\n top: 8px;\n right: 8px;\n opacity: 0;\n transition: opacity 250ms cubic-bezier(0.2, 0, 0, 1);\n}\n.c2-code-viewer-body:hover > .c2-code-viewer-copy, .c2-code-viewer-body:focus-within > .c2-code-viewer-copy,\n.c2-code-viewer-body > .c2-code-viewer-copy.is-copied {\n opacity: 1;\n}\n\n.c2-code-viewer-copy {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: none;\n box-sizing: border-box;\n width: var(--c2-code-viewer__copy--size,28px);\n height: var(--c2-code-viewer__copy--size,28px);\n padding: 0;\n border: none;\n border-radius: var(--c2-code-viewer__copy--border-radius,6px);\n background: var(--c2-code-viewer__copy--background);\n color: var(--c2-code-viewer__copy--color);\n opacity: 0.7;\n cursor: pointer;\n transition: background 250ms cubic-bezier(0.2, 0, 0, 1), opacity 250ms cubic-bezier(0.2, 0, 0, 1);\n}\n.c2-code-viewer-copy svg,\n.c2-code-viewer-copy ::slotted(*) {\n width: var(--c2-code-viewer__copy--icon-size,14px);\n height: var(--c2-code-viewer__copy--icon-size,14px);\n}\n.c2-code-viewer-copy:hover {\n background: var(--c2-code-viewer__copy__hover--background,rgba(127, 127, 127, 0.15));\n opacity: 1;\n}\n.c2-code-viewer-copy:focus-visible {\n outline: 2px solid rgba(2, 101, 220, 0.4);\n outline-offset: 1px;\n opacity: 1;\n}\n.c2-code-viewer-copy.is-copied {\n color: var(--c2-code-viewer__copy__copied--color,rgb(34, 197, 94));\n opacity: 1;\n}\n\npre.shiki {\n margin: 0;\n box-sizing: border-box;\n min-width: 100%;\n width: max-content;\n background: transparent;\n color: inherit;\n font: inherit;\n tab-size: var(--c2-code-viewer--tab-size,2);\n padding-top: var(--c2-code-viewer--padding-top,12px);\n padding-right: var(--c2-code-viewer--padding-right,16px);\n padding-bottom: var(--c2-code-viewer--padding-bottom,12px);\n padding-left: var(--c2-code-viewer--padding-left,16px);\n}\npre.shiki code {\n display: block;\n font: inherit;\n}\npre.shiki .line {\n display: inline-block;\n width: 100%;\n box-sizing: border-box;\n min-height: 1lh;\n}\npre.shiki .line.highlighted {\n background: var(--c2-code-viewer__line__highlighted--background,rgba(99, 102, 241, 0.12));\n border-left: var(--c2-code-viewer__line__highlighted--border-left,3px solid rgb(99, 102, 241));\n margin-left: calc(-1 * var(--c2-code-viewer--padding-left,16px));\n padding-left: calc(var(--c2-code-viewer--padding-left,16px) - 3px);\n width: calc(100% + var(--c2-code-viewer--padding-left,16px) + var(--c2-code-viewer--padding-right,16px));\n margin-right: calc(-1 * var(--c2-code-viewer--padding-right,16px));\n}\npre.shiki.has-line-numbers .line::before {\n counter-increment: line;\n content: counter(line);\n display: inline-block;\n min-width: var(--c2-code-viewer__line-numbers--min-width,2ch);\n margin-right: var(--c2-code-viewer__line-numbers--gap,16px);\n text-align: right;\n color: var(--c2-code-viewer__line-numbers--color);\n opacity: 0.45;\n user-select: none;\n}\npre.shiki .diff-marker {\n user-select: none;\n}\n\n:host([wrap]) pre.shiki {\n width: 100%;\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n.c2-code-viewer-inline {\n font-family: var(--c2-code-viewer--font-family,ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);\n font-size: 0.9em;\n color: var(--c2-code-viewer--color,var(--cv-fg,var(--shiki-light)));\n background: var(--c2-code-viewer--background,var(--cv-bg,var(--shiki-light-bg)));\n padding: 0.1em 0.4em;\n border-radius: 4px;\n border-top: var(--c2-code-viewer--border-top,1px solid rgb(213, 213, 213));\n border-right: var(--c2-code-viewer--border-right,1px solid rgb(213, 213, 213));\n border-bottom: var(--c2-code-viewer--border-bottom,1px solid rgb(213, 213, 213));\n border-left: var(--c2-code-viewer--border-left,1px solid rgb(213, 213, 213));\n}\n\n.dual span {\n color: var(--shiki-light);\n}\n\n:host([color-scheme=dark]) .c2-code-viewer,\n:host([color-scheme=dark]) .c2-code-viewer-inline {\n background: var(--c2-code-viewer--background,var(--cv-bg,var(--shiki-dark-bg)));\n color: var(--c2-code-viewer--color,var(--cv-fg,var(--shiki-dark)));\n}\n:host([color-scheme=dark]) .dual span {\n color: var(--shiki-dark);\n}\n\n@media (prefers-color-scheme: dark) {\n :host(:not([color-scheme=light])) .c2-code-viewer,\n :host(:not([color-scheme=light])) .c2-code-viewer-inline {\n background: var(--c2-code-viewer--background,var(--cv-bg,var(--shiki-dark-bg)));\n color: var(--c2-code-viewer--color,var(--cv-fg,var(--shiki-dark)));\n }\n :host(:not([color-scheme=light])) .dual span {\n color: var(--shiki-dark);\n }\n}\n@media (prefers-reduced-motion: reduce) {\n .c2-code-viewer-copy,\n .c2-code-viewer-body > .c2-code-viewer-copy {\n transition: none;\n }\n}";
12
+ //#endregion
13
+ //#region src/shiki.ts
14
+ var CSS_VARIABLES_THEME = "css-variables";
15
+ var CSS_VARIABLES_PREFIX = "--c2-code-viewer__theme--";
16
+ /** Defaults baked into the `css-variables` theme (a GitHub-light-like palette), overridable per element. */
17
+ var CSS_VARIABLE_DEFAULTS = {
18
+ foreground: "#24292e",
19
+ background: "#ffffff",
20
+ "token-constant": "#005cc5",
21
+ "token-string": "#032f62",
22
+ "token-comment": "#6a737d",
23
+ "token-keyword": "#d73a49",
24
+ "token-parameter": "#24292e",
25
+ "token-function": "#6f42c1",
26
+ "token-string-expression": "#22863a",
27
+ "token-punctuation": "#24292e",
28
+ "token-link": "#032f62"
29
+ };
30
+ var corePromise;
31
+ var pending = /* @__PURE__ */ new Map();
32
+ /** One shared highlighter for every `c2-code-viewer`; grammars and themes are loaded on demand. */
33
+ function getCore() {
34
+ return corePromise ??= createHighlighterCore({
35
+ engine: createJavaScriptRegexEngine({ forgiving: true }),
36
+ langs: [],
37
+ themes: []
38
+ });
39
+ }
40
+ /** Resolves a language id or alias to what the highlighter knows, falling back to `plaintext`. */
41
+ function normalizeLang(lang) {
42
+ const id = (lang ?? "").trim().toLowerCase();
43
+ if (!id || id === "text" || id === "txt" || id === "plaintext" || id === "plain") return "plaintext";
44
+ return id in bundledLanguages ? id : "plaintext";
45
+ }
46
+ async function ensureLanguage(core, lang) {
47
+ if (lang === "plaintext" || core.getLoadedLanguages().includes(lang)) return;
48
+ const key = `lang:${lang}`;
49
+ if (!pending.has(key)) pending.set(key, core.loadLanguage(bundledLanguages[lang]).finally(() => pending.delete(key)));
50
+ await pending.get(key);
51
+ }
52
+ var cssVariablesTheme;
53
+ async function ensureTheme(core, theme) {
54
+ if (core.getLoadedThemes().includes(theme)) return;
55
+ const key = `theme:${theme}`;
56
+ if (!pending.has(key)) {
57
+ const registration = theme === "css-variables" ? cssVariablesTheme ??= createCssVariablesTheme({
58
+ name: CSS_VARIABLES_THEME,
59
+ variablePrefix: CSS_VARIABLES_PREFIX,
60
+ variableDefaults: CSS_VARIABLE_DEFAULTS,
61
+ fontStyle: true
62
+ }) : bundledThemes[theme];
63
+ pending.set(key, core.loadTheme(registration).finally(() => pending.delete(key)));
64
+ }
65
+ await pending.get(key);
66
+ }
67
+ function isKnownTheme(theme) {
68
+ return !!theme && (theme === "css-variables" || theme in bundledThemes);
69
+ }
70
+ /** Highlights `code`; the component stylesheet owns the frame look, the theme only supplies colours. */
71
+ async function highlight({ code, lang, theme, darkTheme, inline = false, lineNumbers = false, startLine = 1, highlightedLines }) {
72
+ const core = await getCore();
73
+ const language = normalizeLang(lang);
74
+ await Promise.all([
75
+ ensureLanguage(core, language),
76
+ ensureTheme(core, theme),
77
+ darkTheme ? ensureTheme(core, darkTheme) : void 0
78
+ ]);
79
+ const themeOptions = darkTheme ? {
80
+ themes: {
81
+ light: theme,
82
+ dark: darkTheme
83
+ },
84
+ defaultColor: false
85
+ } : { theme };
86
+ const html = core.codeToHtml(code, {
87
+ lang: language,
88
+ ...themeOptions,
89
+ transformers: [{
90
+ pre(node) {
91
+ const style = String(node.properties.style ?? "");
92
+ if (!darkTheme) node.properties.style = style.replace(/background-color:/, "--cv-bg:").replace(/(^|;)color:/, "$1--cv-fg:");
93
+ const classes = [...String(node.properties.class ?? "").split(" ").filter(Boolean)];
94
+ if (lineNumbers) classes.push("has-line-numbers");
95
+ if (darkTheme) classes.push("dual");
96
+ node.properties.class = classes.join(" ");
97
+ delete node.properties.tabindex;
98
+ },
99
+ code(node) {
100
+ if (lineNumbers) node.properties.style = `counter-reset: line ${startLine - 1}`;
101
+ },
102
+ line(node, line) {
103
+ if (highlightedLines?.has(line)) this.addClassToHast(node, "highlighted");
104
+ if (language === "diff") {
105
+ const first = node.children[0];
106
+ const text = first?.type === "element" ? first.children[0] : void 0;
107
+ if (text?.type === "text" && (text.value[0] === "+" || text.value[0] === "-")) {
108
+ const marker = text.value[0];
109
+ text.value = text.value.slice(1);
110
+ first.children.unshift({
111
+ type: "element",
112
+ tagName: "span",
113
+ properties: { class: "diff-marker" },
114
+ children: [{
115
+ type: "text",
116
+ value: marker
117
+ }]
118
+ });
119
+ }
120
+ }
121
+ }
122
+ }]
123
+ });
124
+ const style = html.match(/^<pre[^>]*?style="([^"]*)"/)?.[1] ?? "";
125
+ if (inline) return {
126
+ html: html.match(/<code[^>]*>([\s\S]*)<\/code>\s*<\/pre>\s*$/)?.[1] ?? html,
127
+ style
128
+ };
129
+ return {
130
+ html,
131
+ style
132
+ };
133
+ }
134
+ //#endregion
135
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
136
+ function __decorate(decorators, target, key, desc) {
137
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
138
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
139
+ 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;
140
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
141
+ }
142
+ //#endregion
143
+ //#region src/code-viewer.ts
144
+ var LEGACY_LANG_ATTRIBUTE = "langage";
145
+ var CodeViewer = class CodeViewer extends LitElement {
146
+ constructor(..._args) {
147
+ super(..._args);
148
+ this.code = void 0;
149
+ this.language = "plaintext";
150
+ this.theme = "github-light";
151
+ this.darkTheme = void 0;
152
+ this.colorScheme = "auto";
153
+ this.wrap = false;
154
+ this.inline = false;
155
+ this.lineNumbers = false;
156
+ this.startLine = 1;
157
+ this.highlightLines = "";
158
+ this.copyable = false;
159
+ this.slotCode = "";
160
+ this.hasTitle = false;
161
+ this.highlighted = void 0;
162
+ this.copied = false;
163
+ this.highlightTask = new Task(this, {
164
+ args: () => [
165
+ this.source,
166
+ this.language,
167
+ this.theme,
168
+ this.darkTheme,
169
+ this.inline,
170
+ this.lineNumbers,
171
+ this.startLine,
172
+ this.highlightLines
173
+ ],
174
+ task: async ([code, lang, theme, darkTheme, inline, lineNumbers, startLine]) => {
175
+ if (!code) return {
176
+ html: "",
177
+ style: ""
178
+ };
179
+ return highlight({
180
+ code,
181
+ lang,
182
+ theme: isKnownTheme(theme) ? theme : "github-light",
183
+ darkTheme: isKnownTheme(darkTheme) ? darkTheme : void 0,
184
+ inline,
185
+ lineNumbers,
186
+ startLine,
187
+ highlightedLines: this.highlightedLines
188
+ });
189
+ },
190
+ onComplete: (value) => {
191
+ this.highlighted = value;
192
+ }
193
+ });
194
+ }
195
+ static {
196
+ this.styles = unsafeCSS(code_viewer_default);
197
+ }
198
+ /** Accepts the pre-1.0 `langage` attribute. */
199
+ static get observedAttributes() {
200
+ return [...super.observedAttributes, LEGACY_LANG_ATTRIBUTE];
201
+ }
202
+ attributeChangedCallback(name, old, value) {
203
+ if (name === LEGACY_LANG_ATTRIBUTE) {
204
+ if (value !== null) this.language = value;
205
+ return;
206
+ }
207
+ super.attributeChangedCallback(name, old, value);
208
+ }
209
+ /** The code being displayed: `code` or the dedented slot text. */
210
+ get source() {
211
+ if (this.code === void 0) return this.slotCode;
212
+ return this.code.includes("\n") ? this.code : this.code.replace(/\\n/g, "\n");
213
+ }
214
+ get highlightedLines() {
215
+ const lines = /* @__PURE__ */ new Set();
216
+ for (const part of this.highlightLines.split(",")) {
217
+ const [from, to] = part.trim().split("-").map((n) => Number.parseInt(n, 10));
218
+ if (Number.isNaN(from)) continue;
219
+ const last = to === void 0 || Number.isNaN(to) ? from : to;
220
+ for (let i = from; i <= last; i++) lines.add(i);
221
+ }
222
+ return lines;
223
+ }
224
+ willUpdate(changed) {
225
+ if (changed.has("code") || changed.has("slotCode") || changed.has("language") || changed.has("inline")) this.highlighted = void 0;
226
+ }
227
+ firstUpdated() {
228
+ for (const slot of this.renderRoot.querySelectorAll("slot")) this.updateSlot(slot);
229
+ }
230
+ handleSlotChange(event) {
231
+ this.updateSlot(event.target);
232
+ }
233
+ updateSlot(slot) {
234
+ const nodes = slot.assignedNodes({ flatten: true });
235
+ if (slot.name === "title") this.hasTitle = nodes.some((n) => n.nodeType === Node.ELEMENT_NODE || (n.textContent ?? "").trim() !== "");
236
+ else if (slot.name === "") {
237
+ const elements = nodes.filter((n) => n.nodeType === Node.ELEMENT_NODE);
238
+ const parts = (elements.length ? elements : nodes).map((n) => n instanceof HTMLTemplateElement ? n.innerHTML : n.textContent ?? "");
239
+ this.slotCode = dedent(parts.join(""));
240
+ }
241
+ }
242
+ /** Copies the displayed code to the clipboard. */
243
+ async copy() {
244
+ const code = this.source;
245
+ await navigator.clipboard.writeText(code);
246
+ this.copied = true;
247
+ clearTimeout(this.copiedTimer);
248
+ this.copiedTimer = setTimeout(() => this.copied = false, 2e3);
249
+ this.dispatchEvent(new CustomEvent("code-copy", {
250
+ detail: { code },
251
+ bubbles: true,
252
+ composed: true
253
+ }));
254
+ }
255
+ renderCopyButton() {
256
+ return html`
257
+ <button
258
+ class=${classMap({
259
+ "c2-code-viewer-copy": true,
260
+ "is-copied": this.copied
261
+ })}
262
+ type="button"
263
+ aria-label=${this.copied ? "Copied" : "Copy code"}
264
+ @click=${this.copy}
265
+ >
266
+ ${this.copied ? html`<slot name="copied-icon"
267
+ ><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
268
+ <polyline points="20 6 9 17 4 12"></polyline></svg
269
+ ></slot>` : html`<slot name="copy-icon"
270
+ ><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
271
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
272
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg
273
+ ></slot>`}
274
+ </button>
275
+ `;
276
+ }
277
+ renderCode() {
278
+ if (this.highlighted !== void 0) return unsafeHTML(this.highlighted.html);
279
+ return this.inline ? html`${this.source}` : html`<pre
280
+ class="shiki plain ${this.lineNumbers ? "has-line-numbers" : ""}"
281
+ ><code style=${this.lineNumbers ? `counter-reset: line ${this.startLine - 1}` : nothing}>${this.source.split("\n").map((line, i, all) => html`<span class="line">${line}</span>${i < all.length - 1 ? "\n" : ""}`)}</code></pre>`;
282
+ }
283
+ render() {
284
+ const source = html`<slot hidden @slotchange=${this.handleSlotChange}></slot>`;
285
+ if (this.inline) return html`${source}<code
286
+ class="c2-code-viewer-inline ${this.darkTheme ? "dual" : ""}"
287
+ style=${this.highlighted?.style || nothing}
288
+ data-language=${normalizeLang(this.language)}
289
+ >${this.renderCode()}</code
290
+ >`;
291
+ return html`
292
+ ${source}
293
+ <div class=${classMap({
294
+ "c2-code-viewer": true,
295
+ "has-title": this.hasTitle
296
+ })} style=${this.highlighted?.style || nothing}>
297
+ <div class="c2-code-viewer-header" ?hidden=${!this.hasTitle}>
298
+ <slot name="title" @slotchange=${this.handleSlotChange}></slot>
299
+ ${this.copyable ? this.renderCopyButton() : nothing}
300
+ </div>
301
+ <div class="c2-code-viewer-body">${this.renderCode()} ${this.copyable && !this.hasTitle ? this.renderCopyButton() : nothing}</div>
302
+ </div>
303
+ `;
304
+ }
305
+ };
306
+ __decorate([property()], CodeViewer.prototype, "code", void 0);
307
+ __decorate([property({ reflect: true })], CodeViewer.prototype, "language", void 0);
308
+ __decorate([property()], CodeViewer.prototype, "theme", void 0);
309
+ __decorate([property({ attribute: "dark-theme" })], CodeViewer.prototype, "darkTheme", void 0);
310
+ __decorate([property({
311
+ reflect: true,
312
+ attribute: "color-scheme"
313
+ })], CodeViewer.prototype, "colorScheme", void 0);
314
+ __decorate([property({
315
+ type: Boolean,
316
+ reflect: true
317
+ })], CodeViewer.prototype, "wrap", void 0);
318
+ __decorate([property({
319
+ type: Boolean,
320
+ reflect: true
321
+ })], CodeViewer.prototype, "inline", void 0);
322
+ __decorate([property({
323
+ type: Boolean,
324
+ reflect: true,
325
+ attribute: "line-numbers"
326
+ })], CodeViewer.prototype, "lineNumbers", void 0);
327
+ __decorate([property({
328
+ type: Number,
329
+ attribute: "start-line"
330
+ })], CodeViewer.prototype, "startLine", void 0);
331
+ __decorate([property({ attribute: "highlight-lines" })], CodeViewer.prototype, "highlightLines", void 0);
332
+ __decorate([property({
333
+ type: Boolean,
334
+ reflect: true
335
+ })], CodeViewer.prototype, "copyable", void 0);
336
+ __decorate([state()], CodeViewer.prototype, "slotCode", void 0);
337
+ __decorate([state()], CodeViewer.prototype, "hasTitle", void 0);
338
+ __decorate([state()], CodeViewer.prototype, "highlighted", void 0);
339
+ __decorate([state()], CodeViewer.prototype, "copied", void 0);
340
+ CodeViewer = __decorate([customElement("c2-code-viewer")], CodeViewer);
341
+ /** Removes leading/trailing blank lines and the indentation shared by every non-empty line. */
342
+ function dedent(text) {
343
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
344
+ while (lines.length && lines[0].trim() === "") lines.shift();
345
+ while (lines.length && lines[lines.length - 1].trim() === "") lines.pop();
346
+ const indent = Math.min(...lines.filter((l) => l.trim() !== "").map((l) => l.match(/^[ \t]*/)[0].length));
347
+ return lines.map((l) => l.slice(Number.isFinite(indent) ? indent : 0)).join("\n");
348
+ }
349
+ //#endregion
350
+ export { CodeViewer, dedent };
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@c2n/code-viewer",
3
+ "version": "0.0.6",
4
+ "type": "module",
5
+ "main": "dist/code-viewer.js",
6
+ "exports": {
7
+ ".": {
8
+ "default": "./dist/code-viewer.js",
9
+ "types": "./types/src/code-viewer.d.ts"
10
+ },
11
+ "./custom-elements.json": "./custom-elements.json"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "types"
16
+ ],
17
+ "keywords": [
18
+ "code-viewer",
19
+ "web component",
20
+ "lit"
21
+ ],
22
+ "author": "code2nguyen@gmail.com",
23
+ "publishConfig": {
24
+ "registry": "https://registry.npmjs.org",
25
+ "access": "public"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/code2nguyen/web-components.git"
30
+ },
31
+ "scripts": {
32
+ "dev": "vite",
33
+ "build": "wireit",
34
+ "type-check": "wireit"
35
+ },
36
+ "wireit": {
37
+ "type-check": {
38
+ "dependencies": [
39
+ "../../core:build"
40
+ ],
41
+ "command": "tsc -p tsconfig.lib.json --composite false"
42
+ },
43
+ "build": {
44
+ "dependencies": [
45
+ "type-check"
46
+ ],
47
+ "command": "vite build"
48
+ }
49
+ },
50
+ "dependencies": {
51
+ "@c2n/core": "0.0.6",
52
+ "@lit/task": "1.0.3",
53
+ "lit": "3.3.3",
54
+ "shiki": "4.4.3"
55
+ },
56
+ "devDependencies": {
57
+ "@c2n/config": "*",
58
+ "@types/hast": "^3.0.5"
59
+ },
60
+ "customElements": "custom-elements.json",
61
+ "gitHead": "2921054bc75299da66dad11c1e374246ff88a51e"
62
+ }
@@ -0,0 +1,142 @@
1
+ import { LitElement, type PropertyValues } from 'lit';
2
+ import { Task } from '@lit/task';
3
+ import { type HighlightResult, type ThemeName } from './shiki';
4
+ export type { ThemeName } from './shiki';
5
+ /**
6
+ * Syntax-highlighted source code powered by shiki. Grammars and themes are loaded on demand into one shared
7
+ * highlighter, so many viewers on a page cost one engine. The raw code renders immediately as plain text (also on the
8
+ * server) and is upgraded in place once highlighted.
9
+ *
10
+ * Source: the `code` property, or the default slot. Wrap slotted text in a `<pre>` (formatters such as Prettier collapse
11
+ * whitespace inside custom elements but leave `<pre>` alone) and markup samples in a `<template>` so the browser does
12
+ * not parse them. Common indentation is stripped.
13
+ *
14
+ * @tag c2-code-viewer
15
+ *
16
+ * @slot - Source code, in a `<pre>` (text) or a `<template>` (markup). Ignored when the `code` property is set.
17
+ * @slot title - Header label, typically a file name. Shown above the code when present.
18
+ * @slot copy-icon - Replaces the default copy icon.
19
+ * @slot copied-icon - Replaces the default check icon shown after copying.
20
+ *
21
+ * @event {CustomEvent<{ code: string }>} code-copy - Fired after the copy button put the code on the clipboard.
22
+ *
23
+ * @cssproperty {background} --c2-code-viewer--background - Overrides the theme background.
24
+ * @cssproperty {color} --c2-code-viewer--color - Overrides the theme foreground.
25
+ * @cssproperty {font-family} [--c2-code-viewer--font-family=ui-monospace, SFMono-Regular, Menlo, Consolas, monospace]
26
+ * @cssproperty {font-size} [--c2-code-viewer--font-size=13px]
27
+ * @cssproperty {line-height} [--c2-code-viewer--line-height=1.6]
28
+ * @cssproperty {tab-size} [--c2-code-viewer--tab-size=2]
29
+ * @cssproperty {pixel} --c2-code-viewer--max-height - The code scrolls vertically past this height.
30
+ *
31
+ * @cssproperty {padding} [--c2-code-viewer--padding-top=12px]
32
+ * @cssproperty {padding} [--c2-code-viewer--padding-right=16px]
33
+ * @cssproperty {padding} [--c2-code-viewer--padding-bottom=12px]
34
+ * @cssproperty {padding} [--c2-code-viewer--padding-left=16px]
35
+ *
36
+ * @cssproperty {border} [--c2-code-viewer--border-top=1px solid rgb(213, 213, 213)]
37
+ * @cssproperty {border} [--c2-code-viewer--border-right=1px solid rgb(213, 213, 213)]
38
+ * @cssproperty {border} [--c2-code-viewer--border-bottom=1px solid rgb(213, 213, 213)]
39
+ * @cssproperty {border} [--c2-code-viewer--border-left=1px solid rgb(213, 213, 213)]
40
+ * @cssproperty {box-shadow} --c2-code-viewer--box-shadow
41
+ *
42
+ * @cssproperty {border-radius} [--c2-code-viewer--border-top-left-radius=8px]
43
+ * @cssproperty {border-radius} [--c2-code-viewer--border-top-right-radius=8px]
44
+ * @cssproperty {border-radius} [--c2-code-viewer--border-bottom-left-radius=8px]
45
+ * @cssproperty {border-radius} [--c2-code-viewer--border-bottom-right-radius=8px]
46
+ *
47
+ * @cssproperty {background} [--c2-code-viewer__header--background=rgba(127, 127, 127, 0.08)]
48
+ * @cssproperty {color} --c2-code-viewer__header--color
49
+ * @cssproperty {font-family} --c2-code-viewer__header--font-family - Defaults to the code font.
50
+ * @cssproperty {font-size} [--c2-code-viewer__header--font-size=12px]
51
+ * @cssproperty {padding} [--c2-code-viewer__header--padding-top=6px]
52
+ * @cssproperty {padding} [--c2-code-viewer__header--padding-right=8px]
53
+ * @cssproperty {padding} [--c2-code-viewer__header--padding-bottom=6px]
54
+ * @cssproperty {padding} [--c2-code-viewer__header--padding-left=16px]
55
+ * @cssproperty {border} [--c2-code-viewer__header--border-bottom=1px solid rgba(127, 127, 127, 0.2)]
56
+ *
57
+ * @cssproperty {pixel} [--c2-code-viewer__copy--size=28px]
58
+ * @cssproperty {pixel} [--c2-code-viewer__copy--icon-size=14px]
59
+ * @cssproperty {border-radius} [--c2-code-viewer__copy--border-radius=6px]
60
+ * @cssproperty {color} --c2-code-viewer__copy--color - Defaults to the code foreground at 70% opacity.
61
+ * @cssproperty {background} --c2-code-viewer__copy--background
62
+ * @cssproperty {background} [--c2-code-viewer__copy__hover--background=rgba(127, 127, 127, 0.15)]
63
+ * @cssproperty {color} [--c2-code-viewer__copy__copied--color=rgb(34, 197, 94)]
64
+ *
65
+ * @cssproperty {pixel} [--c2-code-viewer__line-numbers--min-width=2ch]
66
+ * @cssproperty {pixel} [--c2-code-viewer__line-numbers--gap=16px]
67
+ * @cssproperty {color} --c2-code-viewer__line-numbers--color - Defaults to the foreground at 45% opacity.
68
+ *
69
+ * @cssproperty {background} [--c2-code-viewer__line__highlighted--background=rgba(99, 102, 241, 0.12)]
70
+ * @cssproperty {border} [--c2-code-viewer__line__highlighted--border-left=3px solid rgb(99, 102, 241)]
71
+ *
72
+ * @cssproperty {color} [--c2-code-viewer__theme--foreground=#24292e] - `css-variables` theme only.
73
+ * @cssproperty {color} [--c2-code-viewer__theme--background=#ffffff] - `css-variables` theme only.
74
+ * @cssproperty {color} [--c2-code-viewer__theme--token-constant=#005cc5] - `css-variables` theme only.
75
+ * @cssproperty {color} [--c2-code-viewer__theme--token-string=#032f62] - `css-variables` theme only.
76
+ * @cssproperty {color} [--c2-code-viewer__theme--token-comment=#6a737d] - `css-variables` theme only.
77
+ * @cssproperty {color} [--c2-code-viewer__theme--token-keyword=#d73a49] - `css-variables` theme only.
78
+ * @cssproperty {color} [--c2-code-viewer__theme--token-parameter=#24292e] - `css-variables` theme only.
79
+ * @cssproperty {color} [--c2-code-viewer__theme--token-function=#6f42c1] - `css-variables` theme only.
80
+ * @cssproperty {color} [--c2-code-viewer__theme--token-string-expression=#22863a] - `css-variables` theme only.
81
+ * @cssproperty {color} [--c2-code-viewer__theme--token-punctuation=#24292e] - `css-variables` theme only.
82
+ * @cssproperty {color} [--c2-code-viewer__theme--token-link=#032f62] - `css-variables` theme only.
83
+ */
84
+ export declare class CodeViewer extends LitElement {
85
+ static styles: import("lit").CSSResult;
86
+ /** Source code. When unset, the default slot's text is used. A literal `\n` counts as a newline when the text has none. */
87
+ code: string | undefined;
88
+ /**
89
+ * Language id or alias (`ts`, `html`, `bash`, `diff`, …). Unknown values render as plain text. (Not `lang`: that is the
90
+ * global attribute for the natural language of the element's text and is read by assistive technology.)
91
+ */
92
+ language: string;
93
+ /** shiki theme name, or `css-variables`. */
94
+ theme: ThemeName;
95
+ /** Second theme for dark mode. When set both palettes are rendered and `color-scheme` picks one. */
96
+ darkTheme: ThemeName | undefined;
97
+ /** Which palette to show when `dark-theme` is set: follow the OS (`auto`), or force one. */
98
+ colorScheme: 'auto' | 'light' | 'dark';
99
+ /** Soft-wrap long lines instead of scrolling horizontally. */
100
+ wrap: boolean;
101
+ /** Render as an inline `<code>` fragment (no frame, no header). */
102
+ inline: boolean;
103
+ lineNumbers: boolean;
104
+ /** First line number when `line-numbers` is on. */
105
+ startLine: number;
106
+ /** Lines to emphasise, 1-based, e.g. `"2-4,7"`. */
107
+ highlightLines: string;
108
+ /** Show a copy-to-clipboard button. */
109
+ copyable: boolean;
110
+ private slotCode;
111
+ private hasTitle;
112
+ private highlighted;
113
+ private copied;
114
+ private copiedTimer;
115
+ /** Accepts the pre-1.0 `langage` attribute. */
116
+ static get observedAttributes(): string[];
117
+ attributeChangedCallback(name: string, old: string | null, value: string | null): void;
118
+ /** The code being displayed: `code` or the dedented slot text. */
119
+ get source(): string;
120
+ private get highlightedLines();
121
+ protected highlightTask: Task<readonly [string, string, ThemeName, ThemeName | undefined, boolean, boolean, number, string], HighlightResult | {
122
+ readonly html: "";
123
+ readonly style: "";
124
+ }>;
125
+ willUpdate(changed: PropertyValues): void;
126
+ firstUpdated(): void;
127
+ private handleSlotChange;
128
+ private updateSlot;
129
+ /** Copies the displayed code to the clipboard. */
130
+ copy(): Promise<void>;
131
+ private renderCopyButton;
132
+ private renderCode;
133
+ render(): import("lit-html").TemplateResult<1>;
134
+ }
135
+ /** Removes leading/trailing blank lines and the indentation shared by every non-empty line. */
136
+ export declare function dedent(text: string): string;
137
+ declare global {
138
+ interface HTMLElementTagNameMap {
139
+ 'c2-code-viewer': CodeViewer;
140
+ }
141
+ }
142
+ //# sourceMappingURL=code-viewer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code-viewer.d.ts","sourceRoot":"","sources":["../../src/code-viewer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAA4B,KAAK,cAAc,EAAE,MAAM,KAAK,CAAA;AAI/E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,OAAO,EAA0C,KAAK,eAAe,EAAE,KAAK,SAAS,EAAE,MAAM,SAAS,CAAA;AAEtG,YAAY,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAIxC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8EG;AACH,qBACa,UAAW,SAAQ,UAAU;IACxC,OAAgB,MAAM,0BAAoB;IAE1C,2HAA2H;IAC/G,IAAI,EAAE,MAAM,GAAG,SAAS,CAAY;IAEhD;;;OAGG;IAC0B,QAAQ,SAAc;IAEnD,4CAA4C;IAChC,KAAK,EAAE,SAAS,CAAiB;IAE7C,oGAAoG;IAC7D,SAAS,EAAE,SAAS,GAAG,SAAS,CAAY;IAEnF,4FAA4F;IACpC,WAAW,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAS;IAEvG,8DAA8D;IAClB,IAAI,UAAQ;IAExD,mEAAmE;IACvB,MAAM,UAAQ;IAEa,WAAW,UAAQ;IAE1F,mDAAmD;IACE,SAAS,SAAI;IAElE,mDAAmD;IACP,cAAc,SAAK;IAE/D,uCAAuC;IACK,QAAQ,UAAQ;IAEnD,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,MAAM,CAAQ;IAE/B,OAAO,CAAC,WAAW,CAA2C;IAE9D,+CAA+C;IAC/C,WAAoB,kBAAkB,aAErC;IAEQ,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAQxF,kEAAkE;IAClE,IAAI,MAAM,IAAI,MAAM,CAInB;IAED,OAAO,KAAK,gBAAgB,GAY3B;IAED,SAAS,CAAC,aAAa;;;OAWrB;IAEO,UAAU,CAAC,OAAO,EAAE,cAAc;IAKlC,YAAY;IAIrB,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,UAAU;IAYlB,kDAAkD;IAC5C,IAAI;IASV,OAAO,CAAC,gBAAgB;IAwBxB,OAAO,CAAC,UAAU;IAUT,MAAM;CAqBhB;AAED,+FAA+F;AAC/F,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAM3C;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,gBAAgB,EAAE,UAAU,CAAA;KAC7B;CACF"}
@@ -0,0 +1,35 @@
1
+ import { type BundledLanguage } from 'shiki/langs';
2
+ import { type BundledTheme } from 'shiki/themes';
3
+ export type { BundledLanguage, BundledTheme };
4
+ /** Built-in shiki theme name, or `css-variables` to colour tokens with the `--c2-code-viewer__theme--*` custom properties. */
5
+ export type ThemeName = BundledTheme | 'css-variables';
6
+ export declare const CSS_VARIABLES_THEME = "css-variables";
7
+ /** Defaults baked into the `css-variables` theme (a GitHub-light-like palette), overridable per element. */
8
+ export declare const CSS_VARIABLE_DEFAULTS: Record<string, string>;
9
+ export interface HighlightOptions {
10
+ code: string;
11
+ lang: string;
12
+ theme: ThemeName;
13
+ /** Second theme: the output carries both palettes as `--shiki-light` / `--shiki-dark` variables. */
14
+ darkTheme?: ThemeName;
15
+ inline?: boolean;
16
+ lineNumbers?: boolean;
17
+ startLine?: number;
18
+ /** 1-based line numbers to mark as highlighted. */
19
+ highlightedLines?: Set<number>;
20
+ }
21
+ /** Resolves a language id or alias to what the highlighter knows, falling back to `plaintext`. */
22
+ export declare function normalizeLang(lang: string | undefined): string;
23
+ export declare function isKnownTheme(theme: string | undefined): theme is ThemeName;
24
+ export interface HighlightResult {
25
+ /** `<pre class="shiki"><code>…</code></pre>`, or only the token spans when `inline`. */
26
+ html: string;
27
+ /**
28
+ * The theme's frame colours as custom properties (`--cv-bg` / `--cv-fg` for one theme, shiki's `--shiki-light(-bg)` /
29
+ * `--shiki-dark(-bg)` for two). Put it on the frame so the stylesheet can use them for header and body alike.
30
+ */
31
+ style: string;
32
+ }
33
+ /** Highlights `code`; the component stylesheet owns the frame look, the theme only supplies colours. */
34
+ export declare function highlight({ code, lang, theme, darkTheme, inline, lineNumbers, startLine, highlightedLines, }: HighlightOptions): Promise<HighlightResult>;
35
+ //# sourceMappingURL=shiki.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shiki.d.ts","sourceRoot":"","sources":["../../src/shiki.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,aAAa,CAAA;AACpE,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAA;AAE/D,YAAY,EAAE,eAAe,EAAE,YAAY,EAAE,CAAA;AAE7C,8HAA8H;AAC9H,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG,eAAe,CAAA;AAEtD,eAAO,MAAM,mBAAmB,kBAAkB,CAAA;AAGlD,4GAA4G;AAC5G,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAYxD,CAAA;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,SAAS,CAAA;IAChB,oGAAoG;IACpG,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mDAAmD;IACnD,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAC/B;AAUD,kGAAkG;AAClG,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAI9D;AAqCD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,IAAI,SAAS,CAE1E;AAED,MAAM,WAAW,eAAe;IAC9B,wFAAwF;IACxF,IAAI,EAAE,MAAM,CAAA;IACZ;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAA;CACd;AAED,wGAAwG;AACxG,wBAAsB,SAAS,CAAC,EAC9B,IAAI,EACJ,IAAI,EACJ,KAAK,EACL,SAAS,EACT,MAAc,EACd,WAAmB,EACnB,SAAa,EACb,gBAAgB,GACjB,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CA2D7C"}