Skip to content

Commit fbc98b2

Browse files
committed
[docs] hyperledger-iroha#89: Add scripts, Vue component, parser extension, update tutorial
Signed-off-by: 6r1d <[email protected]>
1 parent d8374a8 commit fbc98b2

27 files changed

+2211
-5
lines changed

.vitepress/config.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { defineConfig, UserConfig, DefaultTheme } from 'vitepress'
22
import Windi from 'vite-plugin-windicss'
33
import footnote from 'markdown-it-footnote'
44
import customHighlight from './plugins/highlight'
5-
import path from 'path'
5+
import { resolve } from 'path'
66
import { VitePWA } from 'vite-plugin-pwa'
7+
import { snippets_plugin } from './snippet_tabs'
78

89
async function themeConfig() {
910
const cfg: UserConfig = {
@@ -157,6 +158,15 @@ function getGuideSidebar(): DefaultTheme.SidebarGroup[] {
157158
},
158159
],
159160
},
161+
{
162+
text: 'Documenting Iroha',
163+
items: [
164+
{
165+
text: 'Code snippets',
166+
link: '/documenting/snippets',
167+
},
168+
],
169+
},
160170
]
161171
}
162172

@@ -172,7 +182,7 @@ export default defineConfig({
172182
lang: 'en-US',
173183
vite: {
174184
plugins: [
175-
Windi({ config: path.resolve(__dirname, '../windi.config.ts') }),
185+
Windi({ config: resolve(__dirname, '../windi.config.ts') }),
176186
VitePWA({
177187
// Based on: https://evilmartians.com/chronicles/how-to-favicon-in-2021-six-files-that-fit-most-needs
178188
manifest: {
@@ -207,6 +217,7 @@ export default defineConfig({
207217
markdown: {
208218
config(md) {
209219
md.use(footnote)
220+
snippets_plugin(md, {'snippet_root': resolve(__dirname, '../src/snippets/')})
210221
},
211222
},
212223

.vitepress/snippet_tabs.ts

+336
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
"use strict";
2+
3+
import { existsSync, readFileSync } from "fs";
4+
import { resolve, join, basename, dirname } from "path";
5+
6+
const COMP_TAG = "SnippetTabs";
7+
8+
class SnippetAccessError extends Error {
9+
constructor(message: string) {
10+
super(message);
11+
this.name = "SnippetAccessError";
12+
}
13+
}
14+
15+
/**
16+
* A number in {-1, 0, 1} set.
17+
*
18+
* 1 means the tag is opening
19+
* 0 means the tag is self-closing
20+
* -1 means the tag is closing
21+
*/
22+
type TokenNesting = -1 | 0 | 1;
23+
24+
/**
25+
* Redefine the Token type from Markdown-it to avoid importing CJS
26+
* https://markdown-it.github.io/markdown-it/#Token
27+
*
28+
* @typedef {object} Token
29+
*/
30+
type Token = {
31+
// Source map info. Format: [ line_begin, line_end ].
32+
map: number[];
33+
// Used in the renderer to calculate line breaks.
34+
// True for block-level tokens.
35+
// False for inline tokens.
36+
block: boolean;
37+
// '*' or '_' for emphasis, fence string for fence, etc.
38+
markup: string;
39+
// Info string for "fence" tokens
40+
info: string;
41+
// Level change marker
42+
nesting: TokenNesting;
43+
};
44+
45+
/**
46+
// Redefine the StateBlock type from Markdown-it that represents a parser state
47+
// to avoid importing CJS
48+
*
49+
* @typedef {object} StateBlock
50+
*/
51+
type StateBlock = {
52+
line: number;
53+
push(arg0: string, arg1: string, arg2: number): Token;
54+
skipSpaces(pos: number): number;
55+
src: string;
56+
bMarks: number[];
57+
eMarks: number[];
58+
tShift: number[];
59+
sCount: number[];
60+
lineMax: number;
61+
parentType: string;
62+
blkIndent: number;
63+
};
64+
65+
/**
66+
// Redefine a type that (vaguely) represents Markdown-it
67+
*
68+
* @typedef {object} MarkdownIt
69+
*/
70+
type MarkdownIt = {
71+
block: any;
72+
renderer: any;
73+
};
74+
75+
/**
76+
* A function that splits string by new lines
77+
* and trims each of those lines.
78+
*
79+
* @param {string} input - an input string
80+
* @returns {string[]} a list of trimmed lines
81+
*/
82+
function splitAndTrimLines(input: string): Array<string> {
83+
return input.split(/\r?\n/).map((item) => item.trim());
84+
}
85+
86+
/**
87+
* A function that composes a new string out of file paths
88+
* provided to it.
89+
*
90+
* Used by the "snippetLocRender" function to render
91+
* the contents of a Vue component slot.
92+
* This component will then display the snippets.
93+
*
94+
* @param {string[]} paths - a list of file path strings
95+
* @returns {string} a string, composed of file contents
96+
*/
97+
function pathListToStr(paths: string[]): string {
98+
let result = "";
99+
for (let pathId = 0; pathId < paths.length; pathId++) {
100+
const linePath = paths[pathId];
101+
try {
102+
result += readFileSync(linePath);
103+
} catch (err) {
104+
const msg =
105+
`Unable to read a file.\n` +
106+
`Filename: "${basename(linePath)}".\n` +
107+
`Directory path: "${dirname(linePath)}".\n\n` +
108+
`Ensure it exists, its location is correct and its access rights allow to read it.\n` +
109+
`If you did not download the snippets, use the "npm run get_snippets" ` +
110+
`or "pnpm run get_snippets" command.\n` +
111+
`Read more in "Documenting Iroha" → "Code snippets" part of the tutorial.` +
112+
`\n`;
113+
throw new SnippetAccessError(msg);
114+
}
115+
}
116+
return result;
117+
}
118+
119+
/**
120+
* A function that initializes a snippet group Markdown-it plugin.
121+
*/
122+
export function snippets_plugin(md: MarkdownIt, options: Record<string, any>) {
123+
/**
124+
* A function that validates snippet parameters and allows it to be rendered.
125+
* If a path is incorrect, rendering won't happen.
126+
*
127+
* @param {string} params - a parameter string that points out a path to the snippets
128+
* @returns {bool} - whether the snippet directory exists or not
129+
*/
130+
function validateDefault(params: string): boolean {
131+
const snippetPath = resolve(params.trim());
132+
let pathExists = false;
133+
try {
134+
pathExists = existsSync(snippetPath);
135+
} catch (fsLookupError) {
136+
console.error(`Snippet directory lookup error: ${fsLookupError.message}`);
137+
}
138+
return pathExists;
139+
}
140+
141+
/**
142+
* Render a section with a pre-defined wrapper tag
143+
*
144+
* @param {string} tokens - a list of Markdown-It token instances
145+
*/
146+
function snippetRender(tokens: Array<Token>, idx: number): string {
147+
if (tokens[idx].nesting === 1) {
148+
// Render an opening tag
149+
return `<${COMP_TAG}>\n`;
150+
} else {
151+
// Render an closing tag
152+
return `</${COMP_TAG}>\n`;
153+
}
154+
}
155+
156+
/**
157+
* Render slots inside the SnippetTabs Vue component.
158+
*
159+
* Locates the internal path or an updated one,
160+
* outputs the contents of files inside.
161+
*
162+
* @param {Array<Token>} tokens - array of Markdown token instances
163+
* @param {number} idx
164+
* @returns {string} - render results
165+
*/
166+
function snippetLocRender(tokens: Array<Token>, idx: number): string {
167+
const pathStr =
168+
options.snippet_root ||
169+
join(options.snippet_root, tokens[idx - 1].info.trim());
170+
const snippetPath: string = resolve(pathStr);
171+
const paths: string[] = splitAndTrimLines(tokens[idx].info.trim()).map(
172+
(filename) => {
173+
return join(snippetPath, filename);
174+
}
175+
);
176+
return `${pathListToStr(paths)}\n`;
177+
}
178+
179+
options = options || {};
180+
181+
let min_markers: number = 3,
182+
marker_str: string = "+",
183+
marker_char: number = marker_str.charCodeAt(0),
184+
marker_len: number = marker_str.length,
185+
validate: Function = options.validate || validateDefault,
186+
render: Function = snippetRender;
187+
188+
if (
189+
!options.hasOwnProperty("snippet_root") ||
190+
options.snippet_root.constructor.name !== "String"
191+
) {
192+
const errTxt =
193+
"Incorrect configuration. " +
194+
"A correct value for snippet_root is required for snippet_tabs plugin.";
195+
throw new Error(errTxt);
196+
}
197+
198+
function snippet_container(
199+
state: StateBlock,
200+
startLine: number,
201+
endLine: number,
202+
silent: boolean
203+
) {
204+
var pos: number,
205+
nextLine: number,
206+
marker_count: number,
207+
markup: string,
208+
params: string,
209+
token: Token,
210+
old_parent: string,
211+
old_line_max: number,
212+
auto_closed = false,
213+
start: number = state.bMarks[startLine] + state.tShift[startLine],
214+
max: number = state.eMarks[startLine];
215+
216+
// Check out the first character quickly
217+
// to filter out most of non-containers
218+
if (marker_char !== state.src.charCodeAt(start)) {
219+
return false;
220+
}
221+
222+
// Check out the rest of the marker string
223+
for (pos = start + 1; pos <= max; pos++) {
224+
if (marker_str[(pos - start) % marker_len] !== state.src[pos]) {
225+
break;
226+
}
227+
}
228+
229+
marker_count = Math.floor((pos - start) / marker_len);
230+
if (marker_count < min_markers) {
231+
return false;
232+
}
233+
pos -= (pos - start) % marker_len;
234+
235+
markup = state.src.slice(start, pos);
236+
params = state.src.slice(pos, max);
237+
if (!validate(params, markup)) {
238+
return false;
239+
}
240+
241+
// Since start is found, we can report success here in validation mode
242+
if (silent) return true;
243+
244+
// Search for the end of the block
245+
nextLine = startLine;
246+
247+
for (;;) {
248+
nextLine++;
249+
if (nextLine >= endLine) {
250+
// Non-closed block should be autoclosed by end of document.
251+
// Also, block seems to be
252+
// automatically closed by the end of a parent one.
253+
break;
254+
}
255+
256+
start = state.bMarks[nextLine] + state.tShift[nextLine];
257+
max = state.eMarks[nextLine];
258+
259+
if (start < max && state.sCount[nextLine] < state.blkIndent) {
260+
// non-empty line with negative indent should stop the list:
261+
// - ```
262+
// test
263+
break;
264+
}
265+
266+
if (marker_char !== state.src.charCodeAt(start)) {
267+
continue;
268+
}
269+
270+
if (state.sCount[nextLine] - state.blkIndent >= 4) {
271+
// closing fence should be indented less than 4 spaces
272+
continue;
273+
}
274+
275+
for (pos = start + 1; pos <= max; pos++) {
276+
if (marker_str[(pos - start) % marker_len] !== state.src[pos]) {
277+
break;
278+
}
279+
}
280+
281+
// closing code fence must be at least as long as the opening one
282+
if (Math.floor((pos - start) / marker_len) < marker_count) {
283+
continue;
284+
}
285+
286+
// make sure tail has spaces only
287+
pos -= (pos - start) % marker_len;
288+
pos = state.skipSpaces(pos);
289+
290+
if (pos < max) {
291+
continue;
292+
}
293+
294+
// found!
295+
auto_closed = true;
296+
break;
297+
}
298+
299+
old_parent = state.parentType;
300+
old_line_max = state.lineMax;
301+
state.parentType = "snippets";
302+
303+
// Prevent the lazy continuations from ever going past an end marker
304+
state.lineMax = nextLine;
305+
306+
token = state.push("snippets_open", "div", 1);
307+
token.markup = markup;
308+
token.block = true;
309+
token.info = params;
310+
token.map = [startLine, nextLine];
311+
312+
token = state.push("snippet_locations", "div", 1);
313+
token.markup = markup;
314+
token.block = true;
315+
token.info = state.src.slice(
316+
state.bMarks[startLine + 1],
317+
state.bMarks[nextLine]
318+
);
319+
token.map = [startLine, nextLine];
320+
321+
token = state.push("snippets_close", "div", -1);
322+
token.markup = state.src.slice(start, pos);
323+
token.block = true;
324+
325+
state.parentType = old_parent;
326+
state.lineMax = old_line_max;
327+
state.line = nextLine + (auto_closed ? 1 : 0);
328+
329+
return true;
330+
}
331+
332+
md.block.ruler.before("fence", "snippets", snippet_container, {});
333+
md.renderer.rules["snippets_open"] = render;
334+
md.renderer.rules["snippets_close"] = render;
335+
md.renderer.rules["snippet_locations"] = snippetLocRender;
336+
}

0 commit comments

Comments
 (0)