Skip to content

Commit 27ab5b7

Browse files
Garth PickellCopilot
andcommitted
Initial commit
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
0 parents  commit 27ab5b7

6 files changed

Lines changed: 730 additions & 0 deletions

File tree

README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Markdown Docs Reader
2+
3+
This project is a lightweight markdown documentation viewer built for **large, code-heavy doc sets**. It uses Docsify as the rendering base, but the goal is not just to display markdown files in a browser. It is meant to make long-form technical documentation easier to navigate, easier to share at the exact right spot, and easier to copy from when the docs are full of code.
4+
5+
Many markdown viewers are optimized for reading a single document. This one is optimized for reading a **documentation tree**: lots of files, deep navigation, heavy use of relative links, and pages that are revisited as reference material instead of read once from top to bottom.
6+
7+
## Why this exists
8+
9+
Standard markdown viewers usually do the basics well:
10+
11+
- render markdown
12+
- show a sidebar or table of contents
13+
- apply a theme
14+
15+
What they usually do *not* do well is support the workflow of someone moving through engineering docs all day. This reader adds behavior for that workflow:
16+
17+
- keeping navigation in sync with where the reader actually is inside a page
18+
- making deep links useful beyond just the file level
19+
- treating code blocks as reusable working material, not just formatted text
20+
- preserving repository-style document structure instead of forcing docs into a separate publishing model
21+
22+
## Advanced features
23+
24+
| Feature | Why it matters |
25+
| --- | --- |
26+
| Scroll-synced deep links | The URL updates as the reader moves through headings, so a shared link can point to the actual section being read, not just the page. |
27+
| Sidebar state driven by reading position | The active navigation item follows the current section, which makes long pages and nested doc trees easier to stay oriented in. |
28+
| Compact nested navigation | Deeper sidebar levels stay collapsed until they are relevant, reducing noise in dense documentation sets. |
29+
| Segment-level code copy | Code blocks are broken into logical segments so readers can copy just the useful part of an example instead of manually selecting text. |
30+
| Full-snippet copy alongside partial copy | Readers can grab the whole block or a single segment depending on whether they want the complete example or just the command/code they need. |
31+
| Code-first visual treatment | Comments, spacing, and snippet boundaries are surfaced in a way that makes walkthrough-style documentation easier to scan. |
32+
| Repo-friendly relative path handling | The viewer works well with documentation that already lives in a repository and links relatively across folders. |
33+
| Static hosting simplicity | It stays easy to host and preview because it can be served from a basic static web server. |
34+
35+
## Good fit
36+
37+
This viewer is especially useful for:
38+
39+
- internal engineering handbooks
40+
- SDK or platform docs
41+
- architecture and integration guides
42+
- code walkthroughs
43+
- versioned documentation sets
44+
45+
## Development
46+
47+
The [`studio-base`](./studio-base/README.md) folder is an example documentation set for developing and tuning the viewer.
48+
49+
## Run
50+
51+
```bash
52+
python -m http.server 4480
53+
```

plugin.js

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
var __init;
2+
__init();
3+
4+
__init = function() {
5+
let timer;
6+
const list = [];
7+
function scrolling(_, delay = 300) {
8+
if (timer) {
9+
clearTimeout(timer);
10+
}
11+
12+
timer = setTimeout(function() {
13+
timer = undefined;
14+
15+
let last = list[0];
16+
const root = document.querySelector(".content");
17+
const view = root.getBoundingClientRect();
18+
const mid = (view.top + view.bottom) / 2;
19+
for (const el of list) {
20+
const rect = el.getBoundingClientRect();
21+
if (rect.top > view.top - 3) {
22+
if (rect.top < mid) {
23+
last = el;
24+
}
25+
26+
break;
27+
}
28+
29+
last = el;
30+
}
31+
32+
const id = last.id;
33+
const hash = location.hash.replace(/\?.*/, "");
34+
history.replaceState(null, null, `${hash}?id=${id}`);
35+
36+
const current = document.querySelector(".sidebar .active");
37+
if (current) {
38+
current.classList.remove("active");
39+
}
40+
41+
for (const el of document.querySelectorAll(".sidebar a")) {
42+
if (el.href === location.href) {
43+
el.parentElement.classList.add("active");
44+
}
45+
}
46+
}, delay);
47+
}
48+
49+
function scrollToPart() {
50+
const hash = location.hash.replace(/.*\?/, "");
51+
const args = new URLSearchParams(hash);
52+
const id = args.get("id") || "_top";
53+
const target = document.getElementById(id);
54+
const scroller = document.querySelector(".content");
55+
if (target) {
56+
target.scrollIntoView({
57+
block: "start",
58+
behavior: "instant"
59+
});
60+
61+
return;
62+
}
63+
64+
scroller.scrollTo({
65+
top: 0,
66+
behavior: "instant"
67+
});
68+
}
69+
70+
addEventListener("hashchange", scrollToPart);
71+
72+
if (location.hash === "") {
73+
history.replaceState(null, null, "#/home/README.md");
74+
}
75+
76+
let cleanup;
77+
function updateHash() {
78+
if (cleanup) {
79+
cleanup();
80+
}
81+
82+
const root = document.querySelector(".content");
83+
root.addEventListener("scroll", scrolling, { passive: true });
84+
85+
cleanup = function() {
86+
list.length = 0;
87+
root.removeEventListener("scroll", scrolling);
88+
89+
if (timer) {
90+
clearTimeout(timer);
91+
timer = undefined;
92+
}
93+
};
94+
95+
for (const el of document.querySelectorAll(".content h1, .content h2, .content h3")) {
96+
list.push(el);
97+
}
98+
99+
scrolling(null, 0);
100+
}
101+
102+
function addCopyButtons() {
103+
for (const pre of document.querySelectorAll(".content pre code")) {
104+
let text = [];
105+
let line = [];
106+
let prefix = "";
107+
const lines = [];
108+
const walker = document.createTreeWalker(pre, NodeFilter.SHOW_ALL);
109+
while (walker.nextNode()) {
110+
const node = walker.currentNode;
111+
if (node.parentNode === pre) {
112+
line.push(node);
113+
}
114+
115+
if (node.nodeType === Node.TEXT_NODE) {
116+
if (prefix) {
117+
node.data = prefix + node.data;
118+
prefix = "";
119+
}
120+
121+
const parts = node.data.split("\n");
122+
if (parts.length > 1) {
123+
prefix = parts.pop();
124+
node.data = parts.shift();
125+
text.push(node.data);
126+
127+
lines.push([text.join(""), line]);
128+
129+
for (const part of parts) {
130+
lines.push([part, [part]]);
131+
}
132+
133+
text = [];
134+
line = [];
135+
}
136+
else {
137+
text.push(node.data);
138+
}
139+
}
140+
}
141+
142+
if (line.length > 0) {
143+
lines.push([text.join(""), line]);
144+
}
145+
146+
if (prefix) {
147+
lines.push([prefix, [prefix]]);
148+
}
149+
150+
const fullText = lines.map(function([text]) {
151+
return text;
152+
}).join("\n");
153+
154+
pre.innerHTML = "";
155+
156+
const fullCopy = document.createElement("button");
157+
fullCopy.className = "copy-button full-copy-button";
158+
fullCopy.dataset.label = "copy all";
159+
fullCopy.addEventListener("click", function() {
160+
navigator.clipboard.writeText(fullText);
161+
162+
const root = pre.parentElement;
163+
root.classList.add("copying");
164+
setTimeout(function() {
165+
root.classList.remove("copying");
166+
}, 100);
167+
});
168+
pre.parentElement.append(fullCopy);
169+
170+
let group;
171+
for (const [text, line] of lines) {
172+
let comment = false;
173+
let single = false;
174+
let space = false;
175+
let end = false;
176+
if (text[0] === "#") {
177+
end = true;
178+
single = true;
179+
comment = true;
180+
}
181+
182+
if (text.trim() === "") {
183+
end = true;
184+
single = true;
185+
space = true;
186+
}
187+
188+
if (!space && text[0].trim()) {
189+
end = true;
190+
}
191+
192+
if (group && end) {
193+
pre.append(group);
194+
group = undefined;
195+
}
196+
197+
let first = false;
198+
if (!group) {
199+
first = true;
200+
group = document.createElement("div");
201+
group.className = "code-segment";
202+
}
203+
else {
204+
group.append("\n");
205+
}
206+
207+
if (comment) {
208+
group.className = "code-comment";
209+
}
210+
else if (space) {
211+
group.className = "code-space";
212+
}
213+
214+
if (space) {
215+
group.append("\n");
216+
}
217+
else {
218+
for (const node of line) {
219+
group.append(node);
220+
}
221+
}
222+
223+
if (single) {
224+
pre.append(group);
225+
group = undefined;
226+
}
227+
else if (first) {
228+
const copy = document.createElement("button");
229+
copy.className = "copy-button";
230+
copy.dataset.label = "copy";
231+
group.append(copy);
232+
233+
const which = group;
234+
copy.addEventListener("click", function() {
235+
const text = which.innerText;
236+
navigator.clipboard.writeText(text);
237+
238+
which.classList.add("copying");
239+
setTimeout(function() {
240+
which.classList.remove("copying");
241+
}, 100);
242+
});
243+
}
244+
}
245+
246+
if (group) {
247+
pre.append(group);
248+
}
249+
}
250+
}
251+
252+
function fixLinks() {
253+
const root = new URL(location.href);
254+
root.hash = "";
255+
256+
for (const a of document.querySelectorAll("a")) {
257+
const url = new URL(a.href);
258+
const hash = url.hash.replace(/.*\?/, "");
259+
const args = new URLSearchParams(hash);
260+
url.hash = "";
261+
262+
if (root.href === url.href) {
263+
args.delete("id");
264+
265+
if (args.size) {
266+
a.href = "?" + args;
267+
}
268+
}
269+
}
270+
}
271+
272+
const config = window.$docsify || {};
273+
const alias = config.alias || {};
274+
const plugins = config.plugins || [];
275+
window.$docsify = Object.assign(config, {
276+
loadNavbar: true,
277+
maxLevel: 3,
278+
routerMode: "hash",
279+
relativePath: true,
280+
alias: {
281+
".*/_navbar.md": "_navbar.md",
282+
...alias,
283+
},
284+
plugins,
285+
});
286+
287+
plugins.push(function(hook) {
288+
hook.doneEach(scrollToPart);
289+
hook.doneEach(updateHash);
290+
hook.doneEach(addCopyButtons);
291+
hook.doneEach(fixLinks);
292+
});
293+
};
294+
295+
__init();
296+
__init = undefined;

studio-base/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
See [here](./)

studio-base/_navbar.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[![](https://apps.vertigisstudio.com/favicon.ico) Studio Base](/home/README.md)
2+
[Tags and Versions](/home/docs/index.md)
3+
[Host Kit](/kit/README.md)
4+
[Utility Containers](/kit/utils/README.md)

studio-base/index.html

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<link rel="icon" href="https://apps.vertigisstudio.com/favicon.ico">
6+
<title>VertiGIS Studio Base</title>
7+
<meta name="viewport" content="width=device-width, initial-scale=1">
8+
<meta name="theme-color" content="#07131f">
9+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsify@4/lib/themes/dark.css">
10+
<link rel="stylesheet" href="../styles.css">
11+
</head>
12+
<body>
13+
<div id="app"></div>
14+
15+
<script>
16+
function __init() {
17+
const args = new URL(location.href).searchParams;
18+
const tag = args.get("tag") || "gh-pages";
19+
window.$docsify = {
20+
alias: {
21+
"/home/(.*)": `https://raw.githubusercontent.com/gpickell/studio-base/${tag}/$1`,
22+
"/kit/(.*)": "https://raw.githubusercontent.com/vertigis/host-kit/master/$1"
23+
}
24+
}
25+
}
26+
</script>
27+
<script src="https://cdn.jsdelivr.net/npm/docsify@4/lib/docsify.min.js"></script>
28+
<script src="https://cdn.jsdelivr.net/npm/prismjs/components/prism-bash.min.js"></script>
29+
<script src="https://cdn.jsdelivr.net/npm/prismjs/components/prism-yaml.min.js"></script>
30+
<script src="https://cdn.jsdelivr.net/npm/prismjs/components/prism-docker.min.js"></script>
31+
<script src="../plugin.js"></script>
32+
</body>
33+
</html>

0 commit comments

Comments
 (0)