reallysimpledocs 1.0.0-beta.5 → 1.0.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.
@@ -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);