ligarb 0.8.2 → 0.9.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.
- checksums.yaml +4 -4
- data/assets/mermaid_check.mjs +116 -0
- data/assets/style.css +37 -3
- data/docs/help.md +851 -0
- data/lib/ligarb/builder.rb +15 -0
- data/lib/ligarb/chapter.rb +21 -1
- data/lib/ligarb/cli.rb +51 -14
- data/lib/ligarb/config.rb +6 -2
- data/lib/ligarb/github_review.rb +60 -13
- data/lib/ligarb/mermaid_checker.rb +85 -0
- data/lib/ligarb/server.rb +49 -13
- data/lib/ligarb/template.rb +51 -0
- data/lib/ligarb/version.rb +1 -1
- data/templates/book.html.erb +51 -6
- data/templates/github_review/.github/workflows/deploy-book.yml +5 -1
- data/templates/github_review/SETUP.md +3 -1
- metadata +18 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fff2a914d9209b69c1b9c35d4133accfe0d5dcc4e272779959baa9c3bba64cf0
|
|
4
|
+
data.tar.gz: da08e338979034ba836c2321adb4820f91231e942a6bedb0e7ffb31128271c52
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6880ee104849e3904dad11583a8b635dc3bf105acea215df197ab4acbe1dcffa432b5c45c0c471d84424f22c7f287c2b11dc553f987c99d2613948d1f01eb766
|
|
7
|
+
data.tar.gz: c230438379c7ff16c808b38835fe0ea764b88433e109ce0573caba710163ec3efddb86975251faa145525176946a543b70eb44670fc55718998fade9098bf7c7
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Build-time mermaid syntax checker for ligarb.
|
|
2
|
+
//
|
|
3
|
+
// Usage: node mermaid_check.mjs <path/to/mermaid.min.js>
|
|
4
|
+
// stdin: JSON array of {"id": <any>, "text": <mermaid source>}
|
|
5
|
+
// stdout: JSON array of {"id": <any>, "error": <message or null>,
|
|
6
|
+
// "kind": "syntax" | "environment"}
|
|
7
|
+
//
|
|
8
|
+
// Loads the browser UMD bundle of mermaid in Node by stubbing just enough
|
|
9
|
+
// of the DOM (mermaid.parse() only parses; it never renders, but DOMPurify
|
|
10
|
+
// refuses to initialize without something that looks like a document).
|
|
11
|
+
//
|
|
12
|
+
// The DOM stub is intentionally minimal, so it cannot satisfy DOMPurify when a
|
|
13
|
+
// node label contains HTML (e.g. "A[1<br>2]"): sanitizing real markup needs a
|
|
14
|
+
// real DOM tree to walk, which the stub does not provide. That surfaces as a
|
|
15
|
+
// generic JS error (e.g. TypeError "Right-hand side of 'instanceof'..."), NOT a
|
|
16
|
+
// diagram syntax error. classifyError() tells the two apart so callers only
|
|
17
|
+
// warn about genuine mermaid problems and not these harness limitations.
|
|
18
|
+
|
|
19
|
+
const noop = () => {};
|
|
20
|
+
|
|
21
|
+
// Catch-all stub: any property access returns another stub, so the bundle's
|
|
22
|
+
// incidental DOM touches during initialization succeed silently.
|
|
23
|
+
function makeStub(name, overrides = {}) {
|
|
24
|
+
const target = function () {};
|
|
25
|
+
Object.assign(target, overrides);
|
|
26
|
+
return new Proxy(target, {
|
|
27
|
+
get(t, prop) {
|
|
28
|
+
if (prop === Symbol.toPrimitive) return () => "";
|
|
29
|
+
if (prop === "toString") return () => "";
|
|
30
|
+
if (typeof prop === "symbol") return undefined;
|
|
31
|
+
if (!(prop in t)) t[prop] = makeStub(name + "." + String(prop));
|
|
32
|
+
return t[prop];
|
|
33
|
+
},
|
|
34
|
+
set(t, prop, v) {
|
|
35
|
+
t[prop] = v;
|
|
36
|
+
return true;
|
|
37
|
+
},
|
|
38
|
+
apply() {
|
|
39
|
+
return makeStub(name + "()");
|
|
40
|
+
},
|
|
41
|
+
construct() {
|
|
42
|
+
return makeStub("new " + name);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
globalThis.window = globalThis;
|
|
48
|
+
// nodeType 9 = DOCUMENT_NODE; DOMPurify checks it to decide it has a real DOM.
|
|
49
|
+
globalThis.document = makeStub("document", { nodeType: 9 });
|
|
50
|
+
globalThis.navigator = { userAgent: "node" };
|
|
51
|
+
globalThis.addEventListener = noop;
|
|
52
|
+
globalThis.location = { href: "http://localhost/", protocol: "http:" };
|
|
53
|
+
globalThis.Element = function Element() {};
|
|
54
|
+
globalThis.HTMLTemplateElement = function HTMLTemplateElement() {};
|
|
55
|
+
globalThis.Node = function Node() {};
|
|
56
|
+
globalThis.NodeFilter = { SHOW_ELEMENT: 1, SHOW_TEXT: 4, SHOW_COMMENT: 128 };
|
|
57
|
+
globalThis.NamedNodeMap = function NamedNodeMap() {};
|
|
58
|
+
globalThis.HTMLFormElement = function HTMLFormElement() {};
|
|
59
|
+
globalThis.DOMParser = function DOMParser() {
|
|
60
|
+
return makeStub("domparser");
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const { readFileSync } = await import("fs");
|
|
64
|
+
const vm = await import("vm");
|
|
65
|
+
|
|
66
|
+
const mermaidPath = process.argv[2];
|
|
67
|
+
if (!mermaidPath) {
|
|
68
|
+
console.error("usage: node mermaid_check.mjs <mermaid.min.js>");
|
|
69
|
+
process.exit(2);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The bundle starts with "use strict" + top-level `var`, so indirect eval
|
|
73
|
+
// would not create the global binding it expects; a classic script does.
|
|
74
|
+
vm.runInThisContext(readFileSync(mermaidPath, "utf8"), {
|
|
75
|
+
filename: "mermaid.min.js",
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const mermaid = globalThis.mermaid;
|
|
79
|
+
if (!mermaid || typeof mermaid.parse !== "function") {
|
|
80
|
+
console.error("mermaid.parse is not available after loading the bundle");
|
|
81
|
+
process.exit(2);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Decide whether a thrown error is a genuine mermaid diagram problem
|
|
85
|
+
// ("syntax") or an artifact of our minimal DOM stub ("environment").
|
|
86
|
+
//
|
|
87
|
+
// - jison grammar errors carry a structured `.hash` -> syntax
|
|
88
|
+
// - mermaid's typed errors (e.g. UnknownDiagramError) -> syntax
|
|
89
|
+
// - generic JS runtime errors (TypeError/ReferenceError/RangeError/EvalError)
|
|
90
|
+
// with no hash come from the DOM stub -> environment
|
|
91
|
+
// - anything else is reported as syntax, erring toward visibility
|
|
92
|
+
function classifyError(e) {
|
|
93
|
+
if (e && e.hash !== undefined) return "syntax";
|
|
94
|
+
const name = e && e.name;
|
|
95
|
+
if (name && name.endsWith("DiagramError")) return "syntax";
|
|
96
|
+
if (["TypeError", "ReferenceError", "RangeError", "EvalError"].includes(name)) {
|
|
97
|
+
return "environment";
|
|
98
|
+
}
|
|
99
|
+
return "syntax";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const blocks = JSON.parse(readFileSync(0, "utf8"));
|
|
103
|
+
const results = [];
|
|
104
|
+
for (const block of blocks) {
|
|
105
|
+
try {
|
|
106
|
+
await mermaid.parse(block.text);
|
|
107
|
+
results.push({ id: block.id, error: null, kind: "syntax" });
|
|
108
|
+
} catch (e) {
|
|
109
|
+
results.push({
|
|
110
|
+
id: block.id,
|
|
111
|
+
error: String(e && e.message ? e.message : e),
|
|
112
|
+
kind: classifyError(e),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
console.log(JSON.stringify(results));
|
data/assets/style.css
CHANGED
|
@@ -515,7 +515,14 @@ mark.search-highlight {
|
|
|
515
515
|
align-items: flex-start;
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
-
.
|
|
518
|
+
.sidebar-header-actions {
|
|
519
|
+
display: flex;
|
|
520
|
+
gap: 0.35rem;
|
|
521
|
+
flex-shrink: 0;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.theme-toggle,
|
|
525
|
+
.sidebar-collapse {
|
|
519
526
|
background: none;
|
|
520
527
|
border: 1px solid var(--color-border);
|
|
521
528
|
border-radius: 4px;
|
|
@@ -527,11 +534,30 @@ mark.search-highlight {
|
|
|
527
534
|
flex-shrink: 0;
|
|
528
535
|
}
|
|
529
536
|
|
|
530
|
-
.theme-toggle:hover
|
|
537
|
+
.theme-toggle:hover,
|
|
538
|
+
.sidebar-collapse:hover {
|
|
531
539
|
color: var(--color-text);
|
|
532
540
|
background: var(--color-sidebar-hover);
|
|
533
541
|
}
|
|
534
542
|
|
|
543
|
+
/* Persistent collapse on wide screens: hide the sidebar and let content
|
|
544
|
+
reclaim the full width. The floating toggle reappears to bring it back.
|
|
545
|
+
Scoped to wide viewports so it never fights the off-canvas behavior. */
|
|
546
|
+
@media (min-width: 901px) {
|
|
547
|
+
.sidebar-collapsed .sidebar {
|
|
548
|
+
transform: translateX(-100%);
|
|
549
|
+
transition: transform 0.3s ease;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.sidebar-collapsed .content {
|
|
553
|
+
margin-left: 0;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
.sidebar-collapsed .sidebar-toggle {
|
|
557
|
+
display: block;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
535
561
|
/* === Part / Appendix TOC === */
|
|
536
562
|
.toc-part {
|
|
537
563
|
margin-top: 0.5rem;
|
|
@@ -643,7 +669,9 @@ mark.search-highlight {
|
|
|
643
669
|
}
|
|
644
670
|
|
|
645
671
|
/* === Responsive === */
|
|
646
|
-
|
|
672
|
+
/* Off-canvas sidebar for narrow viewports. The 900px breakpoint covers iPad
|
|
673
|
+
portrait (810–834px), which would otherwise keep the fixed desktop sidebar. */
|
|
674
|
+
@media (max-width: 900px) {
|
|
647
675
|
.sidebar {
|
|
648
676
|
transform: translateX(-100%);
|
|
649
677
|
transition: transform 0.3s ease;
|
|
@@ -657,6 +685,12 @@ mark.search-highlight {
|
|
|
657
685
|
display: block;
|
|
658
686
|
}
|
|
659
687
|
|
|
688
|
+
/* The in-header collapse button is only meaningful in the fixed desktop
|
|
689
|
+
layout; off-canvas already hides the sidebar via the floating toggle. */
|
|
690
|
+
.sidebar-collapse {
|
|
691
|
+
display: none;
|
|
692
|
+
}
|
|
693
|
+
|
|
660
694
|
.content {
|
|
661
695
|
margin-left: 0;
|
|
662
696
|
padding: 2rem 1.5rem;
|