reallysimpledocs 1.0.0-beta.5 → 1.0.1

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.
@@ -4,7 +4,6 @@ import ArrowLeft from "lucide-static/icons/arrow-left.svg?raw";
4
4
  import ArrowRight from "lucide-static/icons/arrow-right.svg?raw";
5
5
  import Check from "lucide-static/icons/check.svg?raw";
6
6
  import Copy from "lucide-static/icons/copy.svg?raw";
7
- import Menu from "lucide-static/icons/menu.svg?raw";
8
7
  import PanelLeft from "lucide-static/icons/panel-left.svg?raw";
9
8
  import Search from "lucide-static/icons/search.svg?raw";
10
9
  import ClaudeLogo from "../../icons/logos/claude.svg?raw";
@@ -62,7 +61,11 @@ const actionItems = [
62
61
  ];
63
62
  ---
64
63
 
65
- <BaseLayout title={page.title} description={site.description} pagePath={page.path}>
64
+ <BaseLayout
65
+ title={page.title}
66
+ description={site.description}
67
+ pagePath={page.path}
68
+ >
66
69
  <CommandDialog />
67
70
  <Sidebar menu={sidebar} />
68
71
 
@@ -86,35 +89,30 @@ const actionItems = [
86
89
  </button>
87
90
 
88
91
  <div
89
- class="input-group h-8 w-full min-w-0 max-w-72 cursor-text sm:ml-auto"
92
+ class="input-group h-8 w-full min-w-0 max-w-72 cursor-text ml-auto"
90
93
  role="button"
91
94
  tabindex="0"
92
95
  aria-label="Search docs"
93
96
  onclick="window.rsdOpenCommandSearch ? window.rsdOpenCommandSearch() : document.getElementById('command-search')?.showModal()"
94
97
  onkeydown="if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); window.rsdOpenCommandSearch ? window.rsdOpenCommandSearch() : document.getElementById('command-search')?.showModal(); }"
95
98
  >
96
- <input type="text" placeholder="Search..." readonly tabindex="-1" aria-hidden="true" />
97
- <span data-align="start" aria-hidden="true"><Fragment set:html={searchIcon} /></span>
98
- <span data-align="end" aria-hidden="true"><kbd class="kbd">⌘K</kbd></span>
99
+ <input
100
+ type="text"
101
+ placeholder="Search..."
102
+ readonly
103
+ tabindex="-1"
104
+ aria-hidden="true"
105
+ />
106
+ <span data-align="start" aria-hidden="true"
107
+ ><Fragment set:html={searchIcon} /></span
108
+ >
109
+ <span data-align="end" aria-hidden="true"
110
+ ><kbd class="kbd">⌘K</kbd></span
111
+ >
99
112
  </div>
100
113
 
101
114
  <ContentHeader site={site} page={page} />
102
115
  <ThemeToggle />
103
-
104
- <div class="sm:hidden">
105
- <button
106
- type="button"
107
- class="btn peer group"
108
- data-variant="ghost"
109
- data-size="icon-sm"
110
- aria-label="Toggle sidebar"
111
- aria-expanded="false"
112
- aria-controls="sidebar"
113
- onclick="document.getElementById('sidebar')?.toggle()"
114
- >
115
- <Fragment set:html={Menu} />
116
- </button>
117
- </div>
118
116
  </div>
119
117
  </header>
120
118
 
@@ -122,9 +120,17 @@ const actionItems = [
122
120
  <article class="min-w-0 flex-1 wrap-break-word">
123
121
  <div class="mx-auto w-full max-w-2xl flex-1 space-y-10">
124
122
  <header class="space-y-2">
125
- <div class="flex flex-col-reverse items-start gap-3 sm:flex-row sm:gap-x-2">
126
- <h1 class="mr-auto text-3xl font-semibold tracking-tight sm:text-4xl">{page.title}</h1>
127
- <div class="flex w-full items-center justify-end gap-x-2 sm:mt-1 sm:w-auto">
123
+ <div
124
+ class="flex flex-col-reverse items-start gap-3 sm:flex-row sm:gap-x-2"
125
+ >
126
+ <h1
127
+ class="mr-auto text-3xl font-semibold tracking-tight sm:text-4xl"
128
+ >
129
+ {page.title}
130
+ </h1>
131
+ <div
132
+ class="flex w-full items-center justify-end gap-x-2 sm:mt-1 sm:w-auto"
133
+ >
128
134
  <div role="group" class="button-group">
129
135
  <button
130
136
  type="button"
@@ -143,16 +149,30 @@ const actionItems = [
143
149
  <div role="group" class="button-group">
144
150
  {
145
151
  navigation.prev && (
146
- <a href={navigation.prev.path} class="btn" data-variant="secondary" data-size="icon-sm">
152
+ <a
153
+ href={navigation.prev.path}
154
+ class="btn"
155
+ data-variant="secondary"
156
+ data-size="icon-sm"
157
+ >
147
158
  <span class="sr-only">{navigation.prev.title}</span>
148
159
  <Fragment set:html={ArrowLeft} />
149
160
  </a>
150
161
  )
151
162
  }
152
- {navigation.prev && navigation.next && <hr role="separator" />}
163
+ {
164
+ navigation.prev && navigation.next && (
165
+ <hr role="separator" />
166
+ )
167
+ }
153
168
  {
154
169
  navigation.next && (
155
- <a href={navigation.next.path} class="btn" data-variant="secondary" data-size="icon-sm">
170
+ <a
171
+ href={navigation.next.path}
172
+ class="btn"
173
+ data-variant="secondary"
174
+ data-size="icon-sm"
175
+ >
156
176
  <span class="sr-only">{navigation.next.title}</span>
157
177
  <Fragment set:html={ArrowRight} />
158
178
  </a>
@@ -168,7 +188,12 @@ const actionItems = [
168
188
  <footer class="mt-12 flex items-center">
169
189
  {
170
190
  navigation.prev && (
171
- <a href={navigation.prev.path} class="btn mr-auto" data-variant="secondary" data-size="sm">
191
+ <a
192
+ href={navigation.prev.path}
193
+ class="btn mr-auto"
194
+ data-variant="secondary"
195
+ data-size="sm"
196
+ >
172
197
  <Fragment set:html={ArrowLeft} />
173
198
  {navigation.prev.title}
174
199
  </a>
@@ -176,7 +201,12 @@ const actionItems = [
176
201
  }
177
202
  {
178
203
  navigation.next && (
179
- <a href={navigation.next.path} class="btn ml-auto" data-variant="secondary" data-size="sm">
204
+ <a
205
+ href={navigation.next.path}
206
+ class="btn ml-auto"
207
+ data-variant="secondary"
208
+ data-size="sm"
209
+ >
180
210
  {navigation.next.title}
181
211
  <Fragment set:html={ArrowRight} />
182
212
  </a>
@@ -186,15 +216,18 @@ const actionItems = [
186
216
  </div>
187
217
  </article>
188
218
  <aside class="hidden w-56 xl:block">
189
- <div class="docs-toc sticky top-20 space-y-2 text-sm [&_a]:text-muted-foreground [&_a]:hover:text-foreground [&_a[aria-current='location']]:text-foreground [&_ol]:space-y-2 [&_ol_ol]:mt-2 [&_ol_ol]:pl-3" data-docs-toc>
190
- <h2 class="flex items-center gap-1 font-medium">
191
- On this page
192
- </h2>
219
+ <div
220
+ class="docs-toc sticky top-20 space-y-2 text-sm [&_a]:text-muted-foreground [&_a]:hover:text-foreground [&_a[aria-current='location']]:text-foreground [&_ol]:space-y-2 [&_ol_ol]:mt-2 [&_ol_ol]:pl-3"
221
+ data-docs-toc
222
+ >
223
+ <h2 class="flex items-center gap-1 font-medium">On this page</h2>
193
224
  <ol>
194
225
  {
195
226
  headings.map((heading) => (
196
227
  <li class={heading.depth === 3 ? "pl-3" : ""}>
197
- <a href={`#${heading.id}`} data-toc-link>{heading.text}</a>
228
+ <a href={`#${heading.id}`} data-toc-link>
229
+ {heading.text}
230
+ </a>
198
231
  </li>
199
232
  ))
200
233
  }
@@ -212,7 +245,10 @@ const actionItems = [
212
245
  const originalIcon = icon?.outerHTML || "";
213
246
 
214
247
  try {
215
- const response = await fetch(markdownUrl, { credentials: "same-origin", cache: "no-store" });
248
+ const response = await fetch(markdownUrl, {
249
+ credentials: "same-origin",
250
+ cache: "no-store",
251
+ });
216
252
  if (!response.ok) throw new Error("Failed to fetch markdown");
217
253
  const markdown = await response.text();
218
254
  await writeToClipboard(markdown);
@@ -220,11 +256,16 @@ const actionItems = [
220
256
  icon.outerHTML = checkIcon;
221
257
  window.setTimeout(() => {
222
258
  const currentIcon = button.querySelector("svg");
223
- if (currentIcon && originalIcon) currentIcon.outerHTML = originalIcon;
259
+ if (currentIcon && originalIcon)
260
+ currentIcon.outerHTML = originalIcon;
224
261
  }, 1200);
225
262
  }
226
263
  } catch (_) {
227
- window.open(new URL(markdownUrl, window.location.href).toString(), "_blank", "noopener");
264
+ window.open(
265
+ new URL(markdownUrl, window.location.href).toString(),
266
+ "_blank",
267
+ "noopener",
268
+ );
228
269
  }
229
270
  };
230
271
 
@@ -275,7 +316,8 @@ const actionItems = [
275
316
  let current = entries[0];
276
317
 
277
318
  for (const entry of entries) {
278
- if (entry.heading.getBoundingClientRect().top <= marker) current = entry;
319
+ if (entry.heading.getBoundingClientRect().top <= marker)
320
+ current = entry;
279
321
  else break;
280
322
  }
281
323
 
@@ -302,7 +344,9 @@ const actionItems = [
302
344
  activeFromScroll();
303
345
  window.addEventListener("scroll", queueActiveUpdate, { passive: true });
304
346
  window.addEventListener("resize", queueActiveUpdate);
305
- window.addEventListener("hashchange", () => window.setTimeout(activeFromScroll, 0));
347
+ window.addEventListener("hashchange", () =>
348
+ window.setTimeout(activeFromScroll, 0),
349
+ );
306
350
  }
307
351
 
308
352
  initDocsToc();
@@ -3,7 +3,7 @@ import Moon from "lucide-static/icons/moon.svg?raw";
3
3
  import Sun from "lucide-static/icons/sun.svg?raw";
4
4
  ---
5
5
 
6
- <nav class="hidden items-center gap-2 sm:flex">
6
+ <nav class="flex items-center gap-2">
7
7
  <button
8
8
  type="button"
9
9
  aria-label="Toggle dark mode"
@@ -1,25 +1,33 @@
1
1
  ---
2
2
  import { highlightCode } from "../../lib/highlight.js";
3
+ import { decodeHtml } from "../../lib/html.js";
3
4
 
4
- const { lang = "text", title, class: className = "" } = Astro.props;
5
+ const { lang = "text", title, code, class: className = "" } = Astro.props;
5
6
  const slotHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
6
- const rawCode = decodeSlotText(slotHtml);
7
+ const rawCode = typeof code === "string" ? code.trim() : decodeSlotText(slotHtml);
7
8
  const html = await highlightCode(rawCode, lang);
8
9
 
9
10
  function decodeSlotText(value) {
10
- return String(value || "")
11
+ const text = String(value || "")
11
12
  .replace(/^<p>/, "")
12
- .replace(/<\/p>$/, "")
13
- .replace(/&lt;/g, "<")
14
- .replace(/&gt;/g, ">")
15
- .replace(/&amp;/g, "&")
16
- .replace(/&quot;/g, '"')
17
- .replace(/&#39;/g, "'")
18
- .trim();
13
+ .replace(/<\/p>$/, "");
14
+ return decodeHtml(text).trim();
19
15
  }
20
16
  ---
21
17
 
22
- <div class:list={["code-block", className]} data-code-title={title || undefined}>
23
- {title && <header><span>{title}</span></header>}
24
- <Fragment set:html={html} />
25
- </div>
18
+ {
19
+ title ? (
20
+ <div class:list={["code-frame", className]} data-code-title={title}>
21
+ <header>
22
+ <span>{title}</span>
23
+ </header>
24
+ <div class="code-block">
25
+ <Fragment set:html={html} />
26
+ </div>
27
+ </div>
28
+ ) : (
29
+ <div class:list={["code-block", className]}>
30
+ <Fragment set:html={html} />
31
+ </div>
32
+ )
33
+ }
@@ -1,4 +1,6 @@
1
1
  ---
2
+ import { decodeHtml } from "../../lib/html.js";
3
+
2
4
  const { class: className = "" } = Astro.props;
3
5
  const id = `code-group-${Math.random().toString(36).slice(2)}`;
4
6
  const slotHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
@@ -7,32 +9,123 @@ const defaultValue = normalizedTabs[0]?.value;
7
9
 
8
10
  function parseCodeTabs(html) {
9
11
  const blocks = [];
10
- const pattern =
11
- /<div\b(?=[^>]*\bclass="[^"]*\bcode-block\b[^"]*")(?=[^>]*\bdata-code-title="([^"]*)")[^>]*>([\s\S]*?)<\/div>/g;
12
- let match;
13
- while ((match = pattern.exec(html))) {
14
- const label = decodeHtml(match[1]).trim();
15
- if (!label) continue;
12
+ for (const block of findCodeFrames(html)) {
13
+ const label = decodeHtml(block.title).trim();
14
+ const code = findCodeBlock(block.html);
15
+ if (!label || !code) continue;
16
16
  blocks.push({
17
17
  label,
18
18
  value: label,
19
- html: match[0],
19
+ html: code.html,
20
20
  });
21
21
  }
22
22
  return blocks;
23
23
  }
24
24
 
25
- function decodeHtml(value) {
26
- return String(value || "")
27
- .replace(/&lt;/g, "<")
28
- .replace(/&gt;/g, ">")
29
- .replace(/&amp;/g, "&")
30
- .replace(/&quot;/g, '"')
31
- .replace(/&#39;/g, "'");
25
+ function findCodeFrames(html) {
26
+ const blocks = [];
27
+ let cursor = 0;
28
+
29
+ while (cursor < html.length) {
30
+ const open = findNextDiv(html, cursor);
31
+ if (!open) break;
32
+ const title = attrValue(open.source, "data-code-title");
33
+ if (hasClass(open.source, "code-frame") && title) {
34
+ const close = findMatchingDivClose(html, open.end);
35
+ if (!close) break;
36
+ blocks.push({ title, html: html.slice(open.start, close.end) });
37
+ cursor = close.end;
38
+ continue;
39
+ }
40
+ cursor = open.end;
41
+ }
42
+
43
+ return blocks;
32
44
  }
45
+
46
+ function findCodeBlock(html) {
47
+ let cursor = 0;
48
+
49
+ while (cursor < html.length) {
50
+ const open = findNextDiv(html, cursor);
51
+ if (!open) break;
52
+ if (hasClass(open.source, "code-block")) {
53
+ const close = findMatchingDivClose(html, open.end);
54
+ if (!close) return null;
55
+ return { html: html.slice(open.start, close.end) };
56
+ }
57
+ cursor = open.end;
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ function findMatchingDivClose(html, fromIndex) {
64
+ let depth = 1;
65
+ let cursor = fromIndex;
66
+
67
+ while (cursor < html.length) {
68
+ const tag = findNextDiv(html, cursor);
69
+ if (!tag) return null;
70
+ depth += tag.closing ? -1 : 1;
71
+ if (depth === 0) return tag;
72
+ cursor = tag.end;
73
+ }
74
+
75
+ return null;
76
+ }
77
+
78
+ function findNextDiv(html, fromIndex) {
79
+ let cursor = fromIndex;
80
+ while (cursor < html.length) {
81
+ const start = html.indexOf("<div", cursor);
82
+ const closeStart = html.indexOf("</div", cursor);
83
+ const isClose = closeStart !== -1 && (start === -1 || closeStart < start);
84
+ const tagStart = isClose ? closeStart : start;
85
+ if (tagStart === -1) return null;
86
+
87
+ const end = findTagEnd(html, tagStart);
88
+ if (end === -1) return null;
89
+ return {
90
+ start: tagStart,
91
+ end: end + 1,
92
+ closing: isClose,
93
+ source: html.slice(tagStart, end + 1),
94
+ };
95
+ }
96
+
97
+ return null;
98
+ }
99
+
100
+ function findTagEnd(html, start) {
101
+ let quote = "";
102
+ for (let index = start; index < html.length; index += 1) {
103
+ const char = html[index];
104
+ if (quote) {
105
+ if (char === quote) quote = "";
106
+ continue;
107
+ }
108
+ if (char === '"' || char === "'") {
109
+ quote = char;
110
+ continue;
111
+ }
112
+ if (char === ">") return index;
113
+ }
114
+ return -1;
115
+ }
116
+
117
+ function attrValue(tag, name) {
118
+ const pattern = new RegExp(`\\s${name}=(["'])(.*?)\\1`, "s");
119
+ return tag.match(pattern)?.[2] || "";
120
+ }
121
+
122
+ function hasClass(tag, name) {
123
+ return attrValue(tag, "class").split(/\s+/).includes(name);
124
+ }
125
+
33
126
  ---
34
127
 
35
- <div class:list={["code-group", className]}>
128
+ <div class:list={["tabs-frame", className]}>
36
129
  <header>
37
130
  <div class="tabs">
38
131
  <nav role="tablist" aria-orientation="horizontal" data-variant="line">
@@ -62,6 +155,7 @@ function decodeHtml(value) {
62
155
  return (
63
156
  <div
64
157
  role="tabpanel"
158
+ data-code-panel
65
159
  id={`${id}-panel-${index + 1}`}
66
160
  aria-labelledby={`${id}-tab-${index + 1}`}
67
161
  tabindex="-1"
@@ -7,7 +7,7 @@ const html = await highlightCode(code, lang);
7
7
  const previewClassName = className || "w-fit";
8
8
  ---
9
9
 
10
- <section class="preview">
10
+ <section class="tabs-frame">
11
11
  <header>
12
12
  <div class="tabs">
13
13
  <nav role="tablist" aria-orientation="horizontal" data-variant="line">
@@ -0,0 +1,8 @@
1
+ export function parseCodeMeta(meta) {
2
+ const value = String(meta || "");
3
+ const title = value.match(/(?:^|\s)title=(?:"([^"]*)"|'([^']*)'|([^\s]+))/);
4
+
5
+ return {
6
+ title: title?.[1] ?? title?.[2] ?? title?.[3] ?? "",
7
+ };
8
+ }
@@ -4,6 +4,9 @@ import { marked } from "marked";
4
4
  import GithubSlugger from "github-slugger";
5
5
  import { codeToHtml } from "shiki";
6
6
  import * as lucideIcons from "lucide-static";
7
+ import { transformBasecoatTablesHtml } from "../../astro/rehype-basecoat-tables.js";
8
+ import { parseCodeMeta } from "./code-meta.js";
9
+ import { escapeHtml } from "./html.js";
7
10
  import { normalizeDocMarkdown } from "./markdown-export.js";
8
11
  import { markdownPath, routePath } from "./paths.js";
9
12
 
@@ -102,25 +105,10 @@ export async function renderDoc(config, page) {
102
105
  return `<h${depth} id="${id}" tabindex="-1"><a class="header-anchor" href="#${id}">${text}</a></h${depth}>`;
103
106
  };
104
107
 
105
- renderer.table = (token) => {
106
- const header = renderer.tablerow({
107
- text: token.header.map((cell) => renderer.tablecell(cell)).join(""),
108
- });
109
- const body = token.rows
110
- .map((row) =>
111
- renderer.tablerow({
112
- text: row.map((cell) => renderer.tablecell(cell)).join(""),
113
- }),
114
- )
115
- .join("");
116
- const tbody = body ? `<tbody>${body}</tbody>` : "";
117
- return `<div class="relative my-6 w-full overflow-auto"><table><thead>${header}</thead>${tbody}</table></div>`;
118
- };
119
-
120
108
  const tokens = marked.lexer(content);
121
109
  await highlightCode(tokens);
122
110
  const html = marked.parser(tokens, { renderer });
123
- return { page, html, headings };
111
+ return { page, html: await transformBasecoatTablesHtml(html), headings };
124
112
  }
125
113
 
126
114
  export function getDocHeadings(config, page) {
@@ -374,7 +362,7 @@ function normalizeDocSource(source) {
374
362
  async function highlightCode(tokens) {
375
363
  for (const token of tokens) {
376
364
  if (token.type === "code") {
377
- const html = await highlightToken(token);
365
+ const html = codeBlockHtml(token, await highlightToken(token));
378
366
  Object.assign(token, {
379
367
  type: "html",
380
368
  raw: html,
@@ -390,6 +378,12 @@ async function highlightCode(tokens) {
390
378
  }
391
379
  }
392
380
 
381
+ function codeBlockHtml(token, html) {
382
+ const title = parseCodeMeta(token.lang).title;
383
+ if (!title) return html;
384
+ return `<div class="code-frame" data-code-title="${escapeHtml(title)}"><header><span>${escapeHtml(title)}</span></header><div class="code-block">${html}</div></div>`;
385
+ }
386
+
393
387
  async function highlightToken(token) {
394
388
  try {
395
389
  return await codeToHtml(token.text, {
@@ -6,6 +6,15 @@ export function escapeHtml(value) {
6
6
  .replace(/"/g, "&quot;");
7
7
  }
8
8
 
9
+ export function decodeHtml(value) {
10
+ return String(value ?? "")
11
+ .replace(/&lt;/g, "<")
12
+ .replace(/&gt;/g, ">")
13
+ .replace(/&quot;/g, '"')
14
+ .replace(/&#39;/g, "'")
15
+ .replace(/&amp;/g, "&");
16
+ }
17
+
9
18
  export function attrs(values = {}) {
10
19
  return Object.entries(values)
11
20
  .filter(([, value]) => value !== undefined && value !== null && value !== false)
@@ -56,9 +56,29 @@ export function normalizeMdxComponents(content, context = new Map()) {
56
56
  }
57
57
 
58
58
  function replacePairedComponent(content, name, serialize) {
59
- return content.replace(new RegExp(`<${name}\\b([^>]*)>([\\s\\S]*?)<\\/${name}>`, "g"), (_, attrs, children) =>
60
- serialize(attrs, children),
61
- );
59
+ let output = "";
60
+ let cursor = 0;
61
+ const source = String(content || "");
62
+
63
+ while (cursor < source.length) {
64
+ const open = findNextTag(source, name, cursor);
65
+ if (!open) {
66
+ output += source.slice(cursor);
67
+ break;
68
+ }
69
+
70
+ const close = findMatchingCloseTag(source, open);
71
+ if (!close) {
72
+ output += source.slice(cursor);
73
+ break;
74
+ }
75
+
76
+ output += source.slice(cursor, open.start);
77
+ output += serialize(open.attrs, source.slice(open.end, close.start));
78
+ cursor = close.end;
79
+ }
80
+
81
+ return output;
62
82
  }
63
83
 
64
84
  function serializeChildren(_attrs, children, context = new Map()) {
@@ -155,6 +175,94 @@ function longestBacktickRun(value) {
155
175
  return Math.max(0, ...String(value || "").matchAll(/`+/g).map((match) => match[0].length));
156
176
  }
157
177
 
178
+ function findMatchingCloseTag(source, open) {
179
+ let depth = 1;
180
+ let cursor = open.end;
181
+
182
+ while (cursor < source.length) {
183
+ const tag = findNextTag(source, open.name, cursor);
184
+ if (!tag) return null;
185
+ if (tag.closing) depth -= 1;
186
+ else if (!tag.selfClosing) depth += 1;
187
+ if (depth === 0) return tag;
188
+ cursor = tag.end;
189
+ }
190
+
191
+ return null;
192
+ }
193
+
194
+ function findNextTag(source, name, fromIndex) {
195
+ let cursor = fromIndex;
196
+
197
+ while (cursor < source.length) {
198
+ const start = source.indexOf("<", cursor);
199
+ if (start === -1) return null;
200
+ const tag = readTag(source, start);
201
+ if (!tag) {
202
+ cursor = start + 1;
203
+ continue;
204
+ }
205
+ if (tag.name === name) return tag;
206
+ if (!tag.closing && !tag.selfClosing && tag.name.toLowerCase() === "script") {
207
+ const scriptEnd = source.toLowerCase().indexOf("</script>", tag.end);
208
+ cursor = scriptEnd === -1 ? source.length : scriptEnd + "</script>".length;
209
+ continue;
210
+ }
211
+ cursor = tag.end;
212
+ }
213
+
214
+ return null;
215
+ }
216
+
217
+ function readTag(source, start) {
218
+ let cursor = start + 1;
219
+ let closing = false;
220
+
221
+ if (source[cursor] === "/") {
222
+ closing = true;
223
+ cursor += 1;
224
+ }
225
+
226
+ const nameStart = cursor;
227
+ while (/[$A-Z_a-z0-9-]/.test(source[cursor] || "")) cursor += 1;
228
+ const name = source.slice(nameStart, cursor);
229
+ if (!name) return null;
230
+
231
+ let quote = "";
232
+ while (cursor < source.length) {
233
+ const char = source[cursor];
234
+ if (quote) {
235
+ if (char === "\\" && source[cursor + 1]) {
236
+ cursor += 2;
237
+ continue;
238
+ }
239
+ if (char === quote) quote = "";
240
+ cursor += 1;
241
+ continue;
242
+ }
243
+ if (char === '"' || char === "'" || char === "`") {
244
+ quote = char;
245
+ cursor += 1;
246
+ continue;
247
+ }
248
+ if (char === ">") {
249
+ const sourceTag = source.slice(start, cursor + 1);
250
+ const beforeEnd = sourceTag.trimEnd();
251
+ return {
252
+ start,
253
+ end: cursor + 1,
254
+ name,
255
+ closing,
256
+ selfClosing: beforeEnd.endsWith("/>"),
257
+ attrs: closing ? "" : sourceTag.slice(name.length + 1, -1).replace(/\/$/, ""),
258
+ };
259
+ }
260
+ cursor += 1;
261
+ }
262
+
263
+ return null;
264
+ }
265
+
158
266
  function stripCommonIndent(value) {
159
267
  const lines = String(value || "").replace(/^\n+|\n+$/g, "").split("\n");
160
268
  const indents = lines.filter((line) => line.trim()).map((line) => line.match(/^[ \t]*/)[0].length);