markdown-maker 1.10.1 → 1.10.3
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.
- package/.github/workflows/node.js.yml +21 -26
- package/.vscode/extensions.json +3 -0
- package/.vscode/launch.json +4 -22
- package/.vscode/settings.json +8 -3
- package/.vscode/snippets.code-snippets +6 -8
- package/jest.config.js +7 -0
- package/package.json +41 -64
- package/src/cltool.ts +119 -135
- package/src/commands.ts +238 -244
- package/src/errors.ts +26 -0
- package/src/main.ts +123 -0
- package/src/parser.ts +398 -0
- package/src/templates.ts +5 -1
- package/src/types.ts +33 -0
- package/tests/_test-util.ts +44 -0
- package/tests/advanced.spec.ts +92 -0
- package/tests/basic.spec.ts +68 -0
- package/tests/clargs.spec.ts +50 -0
- package/tests/errors.spec.ts +64 -0
- package/tests/html.spec.ts +23 -0
- package/tests/line.spec.ts +21 -0
- package/tests/marked.spec.ts +40 -0
- package/tests/target.spec.ts +41 -0
- package/tests/vars.spec.ts +45 -0
- package/tsconfig.json +66 -64
- package/prettierrc.yaml +0 -4
- package/src/parse.ts +0 -396
- package/test/advanced.test.js +0 -37
- package/test/basic.test.js +0 -67
- package/test/clargs.test.js +0 -51
- package/test/errors.test.js +0 -47
- package/test/hooks.js +0 -9
- package/test/hooks.test.js +0 -114
- package/test/html.test.js +0 -37
- package/test/line.test.js +0 -21
- package/test/marked.test.js +0 -43
- package/test/target.test.js +0 -43
- package/test/tester.test.js +0 -41
- package/test/vars.test.js +0 -49
package/src/main.ts
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
import path from "path";
|
2
|
+
import { WebSocketServer } from "ws";
|
3
|
+
import * as fs from "fs";
|
4
|
+
|
5
|
+
import choki from "chokidar";
|
6
|
+
import Parser from "./parser";
|
7
|
+
|
8
|
+
/* for adding colours to strings */
|
9
|
+
import { enable as ColorsEnable } from "colors.ts";
|
10
|
+
ColorsEnable();
|
11
|
+
|
12
|
+
import { argParser, CommandLineArgs, ParserOptions } from "./cltool";
|
13
|
+
const configFileName = ".mdmconfig.json";
|
14
|
+
|
15
|
+
function main() {
|
16
|
+
let clargs: CommandLineArgs;
|
17
|
+
let server: WebSocketServer | undefined;
|
18
|
+
|
19
|
+
/* Read config file or parse args from cmd-line */
|
20
|
+
if (fs.existsSync(configFileName)) {
|
21
|
+
let data: CommandLineArgs = JSON.parse(
|
22
|
+
fs.readFileSync(configFileName).toString()
|
23
|
+
).opts;
|
24
|
+
|
25
|
+
let args: (string | number)[] = [];
|
26
|
+
Object.entries(data).forEach(([key, value]) => {
|
27
|
+
if (key != "src" && value !== false) {
|
28
|
+
args.push("--" + key);
|
29
|
+
}
|
30
|
+
if (typeof value != "boolean") {
|
31
|
+
args.push(value);
|
32
|
+
}
|
33
|
+
});
|
34
|
+
|
35
|
+
/* We skip [0] and [1], as it is the binary and source file, even when compiled*/
|
36
|
+
for (let i = 2; i < process.argv.length; i++)
|
37
|
+
args.push(process.argv[i]);
|
38
|
+
|
39
|
+
clargs = argParser.parse_args(args.map((x) => x.toString()));
|
40
|
+
} else {
|
41
|
+
clargs = argParser.parse_args();
|
42
|
+
}
|
43
|
+
|
44
|
+
/* if src is init, create config file and exit */
|
45
|
+
if (clargs.src == "init") {
|
46
|
+
const template = fs.readFileSync(
|
47
|
+
path.join(
|
48
|
+
__dirname,
|
49
|
+
"..",
|
50
|
+
"src",
|
51
|
+
"templates",
|
52
|
+
"configTemplate.json"
|
53
|
+
)
|
54
|
+
);
|
55
|
+
fs.writeFileSync(configFileName, template);
|
56
|
+
fs.writeFileSync("main.md", "# Main\n");
|
57
|
+
return;
|
58
|
+
}
|
59
|
+
|
60
|
+
/* helper method for calling parser */
|
61
|
+
const compile = (source, output, cb?) => {
|
62
|
+
/* load data from file, if it exists,
|
63
|
+
* otherwise, interpret as string */
|
64
|
+
|
65
|
+
const parser = new Parser(source, clargs);
|
66
|
+
parser.to(output, (file) => {
|
67
|
+
console.log(`Compiled ${file}`.green);
|
68
|
+
if (cb) cb();
|
69
|
+
});
|
70
|
+
return parser;
|
71
|
+
};
|
72
|
+
|
73
|
+
const internalCooldown = 1000;
|
74
|
+
function watcher(_, path: string) {
|
75
|
+
const now = Date.now();
|
76
|
+
|
77
|
+
if (!this.time) this.time = now;
|
78
|
+
if (now - this.time < internalCooldown) return;
|
79
|
+
console.log(`Detected change in ${path}...`);
|
80
|
+
try {
|
81
|
+
compile(clargs.src, clargs.output, () => {
|
82
|
+
/* after compile, send refresh command to clients */
|
83
|
+
server.clients.forEach((client) => {
|
84
|
+
if (client.OPEN) client.send("refresh");
|
85
|
+
});
|
86
|
+
});
|
87
|
+
} catch (e) {
|
88
|
+
console.log(e.message);
|
89
|
+
}
|
90
|
+
|
91
|
+
this.time = now;
|
92
|
+
}
|
93
|
+
|
94
|
+
/* in case source is a directory, look for entry in directory */
|
95
|
+
if (fs.existsSync(clargs.src) && fs.lstatSync(clargs.src).isDirectory()) {
|
96
|
+
clargs.src = path.join(clargs.src, clargs.entry);
|
97
|
+
}
|
98
|
+
|
99
|
+
if (clargs.debug) console.dir(clargs);
|
100
|
+
|
101
|
+
/* compile once if not watching
|
102
|
+
otherwise watch the folder and recompile on change */
|
103
|
+
if (!clargs.watch) compile(clargs.src, clargs.output);
|
104
|
+
else {
|
105
|
+
const srcDirName = path.dirname(clargs.src);
|
106
|
+
console.log(`Watching ${srcDirName} for changes...`.yellow);
|
107
|
+
server = new WebSocketServer({ port: 7788 });
|
108
|
+
|
109
|
+
const _watcher = choki.watch(srcDirName).on("all", watcher);
|
110
|
+
try {
|
111
|
+
compile(clargs.src, clargs.output);
|
112
|
+
} catch (e) {
|
113
|
+
console.log(e.message);
|
114
|
+
}
|
115
|
+
}
|
116
|
+
}
|
117
|
+
export default {
|
118
|
+
Parser,
|
119
|
+
};
|
120
|
+
/* main entrypoint */
|
121
|
+
if (require.main === module) {
|
122
|
+
main();
|
123
|
+
}
|
package/src/parser.ts
ADDED
@@ -0,0 +1,398 @@
|
|
1
|
+
import fs from "fs"; /* for handling reading of files */
|
2
|
+
import path from "path"; /* for handling file paths */
|
3
|
+
|
4
|
+
import marked from "marked";
|
5
|
+
|
6
|
+
import { Command, commands, load_extensions } from "./commands";
|
7
|
+
import {
|
8
|
+
argParser,
|
9
|
+
IncompleteCommandLineArgs,
|
10
|
+
IncompleteParserOptions,
|
11
|
+
ParserOptions,
|
12
|
+
} from "./cltool";
|
13
|
+
import { MDMError, MDMNonParserError, MDMWarning } from "./errors";
|
14
|
+
import { CommandGroupType, TaggedElement, TargetType } from "./types";
|
15
|
+
|
16
|
+
/* parse some md
|
17
|
+
* recursively with extra options */
|
18
|
+
class Parser {
|
19
|
+
file: string;
|
20
|
+
parent?: Parser;
|
21
|
+
line_num: number;
|
22
|
+
wd: string;
|
23
|
+
wd_full: string;
|
24
|
+
blobs: {
|
25
|
+
[key: number]: string | undefined;
|
26
|
+
};
|
27
|
+
opts: ParserOptions;
|
28
|
+
raw: string;
|
29
|
+
|
30
|
+
static TOKEN = "#md";
|
31
|
+
|
32
|
+
constructor(
|
33
|
+
filename: string,
|
34
|
+
clargs?: IncompleteCommandLineArgs,
|
35
|
+
opts?: IncompleteParserOptions
|
36
|
+
) {
|
37
|
+
/* this.working_directory */
|
38
|
+
this.file = filename;
|
39
|
+
|
40
|
+
this.line_num = 0;
|
41
|
+
this.wd = path.dirname(filename);
|
42
|
+
this.wd_full = path.resolve(this.wd);
|
43
|
+
|
44
|
+
/* finished blob */
|
45
|
+
this.blobs = {};
|
46
|
+
|
47
|
+
if (!clargs) {
|
48
|
+
clargs = argParser.parse_args([filename]);
|
49
|
+
}
|
50
|
+
|
51
|
+
/* get default options, and overwrite with the ones present
|
52
|
+
in the arguments */
|
53
|
+
this.opts = defaultParserOptions();
|
54
|
+
Object.assign(this.opts, clargs);
|
55
|
+
Object.assign(this.opts, opts);
|
56
|
+
|
57
|
+
this.raw = this.opts.isFileCallback(filename) || filename;
|
58
|
+
}
|
59
|
+
|
60
|
+
private parse() {
|
61
|
+
load_extensions(this);
|
62
|
+
if (this.opts.verbose || this.opts.debug) {
|
63
|
+
console.log(
|
64
|
+
`parsing ${this.file}: depth=${this.opts.depth}`.magenta
|
65
|
+
);
|
66
|
+
}
|
67
|
+
|
68
|
+
if (this.opts.debug) {
|
69
|
+
console.log("Parsing options:");
|
70
|
+
console.log(this.opts);
|
71
|
+
}
|
72
|
+
|
73
|
+
/* reset sections for beginning parse */
|
74
|
+
if (this.opts.depth === 0) this.opts.secs = [];
|
75
|
+
let __blob = this.raw;
|
76
|
+
|
77
|
+
/* apply preproccessing to raw file */
|
78
|
+
__blob = this.preprocess(__blob);
|
79
|
+
|
80
|
+
/* main parser instance call */
|
81
|
+
__blob = this.mainparse(__blob);
|
82
|
+
|
83
|
+
/**
|
84
|
+
* apply postprocessing after */
|
85
|
+
__blob = this.postprocess(__blob);
|
86
|
+
|
87
|
+
return __blob;
|
88
|
+
}
|
89
|
+
|
90
|
+
private mainparse(blob: string) {
|
91
|
+
if (this.opts.verbose || this.opts.debug) {
|
92
|
+
console.debug(`beginning mainparse of '${this.file}'`.blue);
|
93
|
+
}
|
94
|
+
|
95
|
+
/* main parser instance loop */
|
96
|
+
blob.split("\n").forEach((line, lnum) => {
|
97
|
+
this.line_num = lnum;
|
98
|
+
|
99
|
+
/* if line looks like a title */
|
100
|
+
const titleMatch = line.trim().match(/^(#+) (.+)$/);
|
101
|
+
|
102
|
+
if (titleMatch) {
|
103
|
+
if (this.opts.verbose || this.opts.debug)
|
104
|
+
console.log("found toc element: " + line);
|
105
|
+
|
106
|
+
/* implement toc level */
|
107
|
+
let level = titleMatch[1].length;
|
108
|
+
let title = titleMatch[2];
|
109
|
+
|
110
|
+
this.opts.secs.push({ level, title });
|
111
|
+
|
112
|
+
if (this.opts.debug) {
|
113
|
+
console.log("updated sections:", { level, title });
|
114
|
+
}
|
115
|
+
}
|
116
|
+
});
|
117
|
+
|
118
|
+
return this.parse_commands(blob, commands.parse);
|
119
|
+
}
|
120
|
+
|
121
|
+
private preprocess(blob: string) {
|
122
|
+
if (this.opts.verbose || this.opts.debug) {
|
123
|
+
console.debug(`beginning preprocess of '${this.file}'`.blue);
|
124
|
+
}
|
125
|
+
|
126
|
+
return this.parse_commands(blob, commands.preparse);
|
127
|
+
}
|
128
|
+
|
129
|
+
private postprocess(blob: string) {
|
130
|
+
if (this.opts.verbose || this.opts.debug) {
|
131
|
+
console.debug(`beginning postprocess of '${this.file}'`.blue);
|
132
|
+
}
|
133
|
+
|
134
|
+
blob = this.parse_commands(blob, commands.postparse);
|
135
|
+
|
136
|
+
/* remove double empty lines */
|
137
|
+
blob = this.remove_double_blank_lines(blob);
|
138
|
+
blob = blob.trimEnd() + "\n\n";
|
139
|
+
return blob;
|
140
|
+
}
|
141
|
+
|
142
|
+
private parse_commands(blob: string, commands: Command[]) {
|
143
|
+
commands.forEach((command) => {
|
144
|
+
/* Add global flag to RegExp */
|
145
|
+
const re = new RegExp(
|
146
|
+
command.validator.source,
|
147
|
+
(command.validator.flags || "") + "g"
|
148
|
+
);
|
149
|
+
|
150
|
+
const replacer = (args: RegExpExecArray) => {
|
151
|
+
try {
|
152
|
+
return command.act(args, this) || "";
|
153
|
+
} catch (error) {
|
154
|
+
switch (true) {
|
155
|
+
case error instanceof MDMError:
|
156
|
+
throw error;
|
157
|
+
case error instanceof MDMWarning:
|
158
|
+
console.warn(error.message);
|
159
|
+
return `**Warning: ${error.message}**`;
|
160
|
+
default:
|
161
|
+
console.error(error);
|
162
|
+
throw error;
|
163
|
+
}
|
164
|
+
}
|
165
|
+
};
|
166
|
+
|
167
|
+
/* */
|
168
|
+
|
169
|
+
let match: RegExpExecArray | null;
|
170
|
+
while ((match = re.exec(blob)) !== null) {
|
171
|
+
blob =
|
172
|
+
blob.slice(0, match.index) +
|
173
|
+
replacer(match) +
|
174
|
+
blob.slice(match.index + match[0].length);
|
175
|
+
}
|
176
|
+
});
|
177
|
+
return blob;
|
178
|
+
}
|
179
|
+
|
180
|
+
/* Parse all commands sequentially on a sub-blob */
|
181
|
+
private parse_all_commands(blob: string, commands: CommandGroupType) {
|
182
|
+
blob = this.parse_commands(blob, commands.preparse);
|
183
|
+
blob = this.parse_commands(blob, commands.parse);
|
184
|
+
blob = this.parse_commands(blob, commands.postparse);
|
185
|
+
return blob;
|
186
|
+
}
|
187
|
+
|
188
|
+
titleId(title: string) {
|
189
|
+
const sep = this.opts.use_underscore ? "_" : "-";
|
190
|
+
|
191
|
+
title = title
|
192
|
+
.toLowerCase()
|
193
|
+
.replace(/[^\w\s]+/g, "")
|
194
|
+
.replace(/[\s_]+/g, sep);
|
195
|
+
return title;
|
196
|
+
}
|
197
|
+
|
198
|
+
get_toc() {
|
199
|
+
let __blob = [];
|
200
|
+
let tabSize = 2;
|
201
|
+
const beg = "* ";
|
202
|
+
const hor = " ".repeat(tabSize);
|
203
|
+
|
204
|
+
this.opts.secs.forEach((sec) => {
|
205
|
+
if (sec.level > this.opts.toc_level) return;
|
206
|
+
let title = sec.title.replace(/_/g, " ");
|
207
|
+
title = this.parse_all_commands(title, commands);
|
208
|
+
const link = this.titleId(title);
|
209
|
+
|
210
|
+
let __line =
|
211
|
+
hor.repeat(Math.max(sec.level - 1, 0)) +
|
212
|
+
beg +
|
213
|
+
`[${title}](#${link})`;
|
214
|
+
|
215
|
+
__blob.push(__line);
|
216
|
+
});
|
217
|
+
return __blob.join("\n");
|
218
|
+
}
|
219
|
+
|
220
|
+
add_hook(
|
221
|
+
name: string,
|
222
|
+
hook: (map: { [key: string]: TaggedElement }) => void
|
223
|
+
) {
|
224
|
+
if (this.opts.hooks[name] != undefined)
|
225
|
+
throw new MDMNonParserError(`Hook "${name}" already exists!`);
|
226
|
+
this.opts.hooks[name] = hook;
|
227
|
+
}
|
228
|
+
|
229
|
+
private line_num_from_index(index: number) {
|
230
|
+
return this.raw.substring(0, index).split("\n").length;
|
231
|
+
}
|
232
|
+
|
233
|
+
private remove_double_blank_lines(blob) {
|
234
|
+
/* replace all triple newlines, and EOF by double newline */
|
235
|
+
blob = blob.replace(/(\r\n|\n){3,}/g, "\n\n");
|
236
|
+
|
237
|
+
return blob;
|
238
|
+
}
|
239
|
+
|
240
|
+
/* output the parsed document to bundle */
|
241
|
+
to(bundleName: string, callback: (fileName: string) => void) {
|
242
|
+
const dir = path.dirname(bundleName);
|
243
|
+
if (callback === undefined) callback = () => {};
|
244
|
+
|
245
|
+
if (!fs.existsSync(dir)) {
|
246
|
+
fs.mkdirSync(dir, { recursive: true });
|
247
|
+
}
|
248
|
+
|
249
|
+
if (!this.opts.html) {
|
250
|
+
this.get(TargetType.MARKDOWN, (blob) => {
|
251
|
+
fs.writeFile(bundleName, blob, () => callback(bundleName));
|
252
|
+
});
|
253
|
+
} else {
|
254
|
+
const htmlFileName = bundleName.replace(".md", ".html");
|
255
|
+
fs.writeFile(htmlFileName, this.html(), () =>
|
256
|
+
callback(htmlFileName)
|
257
|
+
);
|
258
|
+
}
|
259
|
+
}
|
260
|
+
|
261
|
+
html() {
|
262
|
+
const htmlFormatted = marked
|
263
|
+
.parse(this.get(TargetType.HTML))
|
264
|
+
.toString();
|
265
|
+
if (this.opts.watch) {
|
266
|
+
return (
|
267
|
+
`<script>` +
|
268
|
+
`w=new WebSocket("ws:localhost:7788");` +
|
269
|
+
`w.addEventListener("message",(e)=>` +
|
270
|
+
` {if(e.data=="refresh")location.reload();}` +
|
271
|
+
`);` +
|
272
|
+
`</script>\n` +
|
273
|
+
htmlFormatted
|
274
|
+
);
|
275
|
+
}
|
276
|
+
return htmlFormatted;
|
277
|
+
}
|
278
|
+
|
279
|
+
createChild(file: string) {
|
280
|
+
return new Parser(file, undefined, {
|
281
|
+
parent: this,
|
282
|
+
depth: this.opts.depth + 1,
|
283
|
+
...this.opts,
|
284
|
+
});
|
285
|
+
}
|
286
|
+
|
287
|
+
get(targetType?: TargetType, callback?: (blob: string) => void): string {
|
288
|
+
/* If target type is undefined, markdown is the default */
|
289
|
+
if (targetType === undefined) targetType = TargetType.MARKDOWN;
|
290
|
+
if (this.blobs[targetType]) {
|
291
|
+
if (callback) {
|
292
|
+
callback(this.blobs[targetType]);
|
293
|
+
}
|
294
|
+
return this.blobs[targetType];
|
295
|
+
} else {
|
296
|
+
try {
|
297
|
+
this.opts.targetType = targetType;
|
298
|
+
let blob = this.parse();
|
299
|
+
this.opts.targetType = undefined;
|
300
|
+
if (callback) callback(blob);
|
301
|
+
return blob;
|
302
|
+
} catch (error) {
|
303
|
+
/* Compile a traceback of error */
|
304
|
+
let traceback = "";
|
305
|
+
let p: Parser = this;
|
306
|
+
|
307
|
+
do {
|
308
|
+
if (error instanceof MDMError)
|
309
|
+
traceback += `\n...on line ${p.line_num_from_index(
|
310
|
+
error.match.index
|
311
|
+
)} in ${p.file}`.grey(15);
|
312
|
+
else
|
313
|
+
traceback +=
|
314
|
+
`\n...on line ${p.line_num} in ${p.file}`.grey(15);
|
315
|
+
if (p.parent) p = p.parent;
|
316
|
+
} while (p.parent);
|
317
|
+
|
318
|
+
error.message += traceback;
|
319
|
+
|
320
|
+
/* only interested in node stacktrace when debugging */
|
321
|
+
if (!this.opts.debug) error.stack = "";
|
322
|
+
|
323
|
+
if (this.opts.only_warn) console.error(error);
|
324
|
+
else throw error;
|
325
|
+
}
|
326
|
+
}
|
327
|
+
}
|
328
|
+
}
|
329
|
+
|
330
|
+
function defaultParserOptions(): ParserOptions {
|
331
|
+
return {
|
332
|
+
defs: {},
|
333
|
+
secs: [],
|
334
|
+
args: [],
|
335
|
+
depth: 0,
|
336
|
+
verbose: false,
|
337
|
+
debug: false,
|
338
|
+
max_depth: 5,
|
339
|
+
use_underscore: false,
|
340
|
+
toc_level: 3,
|
341
|
+
allow_undefined: false,
|
342
|
+
html: false,
|
343
|
+
watch: false,
|
344
|
+
targetType: undefined,
|
345
|
+
only_warn: false,
|
346
|
+
parent: undefined,
|
347
|
+
hooks: {},
|
348
|
+
isFileCallback: (f) => {
|
349
|
+
if (!fs.existsSync(f)) return false;
|
350
|
+
return fs.readFileSync(f, "utf-8") + "\n";
|
351
|
+
},
|
352
|
+
};
|
353
|
+
}
|
354
|
+
|
355
|
+
export function splice(
|
356
|
+
str: string,
|
357
|
+
startIndex: number,
|
358
|
+
width: number,
|
359
|
+
newSubStr: string
|
360
|
+
) {
|
361
|
+
const start = str.slice(0, startIndex);
|
362
|
+
const end = str.slice(startIndex + width);
|
363
|
+
return start + newSubStr + end;
|
364
|
+
}
|
365
|
+
|
366
|
+
/* add extention to marked for classed blockquotes*/
|
367
|
+
marked.use({
|
368
|
+
renderer: {
|
369
|
+
blockquote(quote: string) {
|
370
|
+
/* find the ending, and if not, return the default */
|
371
|
+
const ending = quote.match(/\{(.+)\}\s*<\/p>/);
|
372
|
+
if (!ending) return `<blockquote>${quote}</blockquote>`;
|
373
|
+
|
374
|
+
const args = ending[1].split(" ");
|
375
|
+
|
376
|
+
const classes = args.filter((arg) => arg.startsWith("."));
|
377
|
+
const id = args.filter((arg) => arg.startsWith("#"));
|
378
|
+
|
379
|
+
const classNames = classes.map((c) => c.slice(1));
|
380
|
+
const classText =
|
381
|
+
classes.length > 0 ? `class="${classNames.join(" ")}"` : "";
|
382
|
+
const idText = id.length > 0 ? `id="${id[0].slice(1)}"` : "";
|
383
|
+
|
384
|
+
/* remove the ending from the quote */
|
385
|
+
quote = quote.replace(/\{(.+)\}\s*<\/p>/, "</p>");
|
386
|
+
|
387
|
+
return `<blockquote ${classText} ${idText}>\n${quote.trim()}</blockquote>`;
|
388
|
+
},
|
389
|
+
heading(text: string, level: number) {
|
390
|
+
/* add an id to each heading */
|
391
|
+
return `<h${level} id="${text
|
392
|
+
.replace(/ /g, "-")
|
393
|
+
.toLowerCase()}">${text}</h${level}>`;
|
394
|
+
},
|
395
|
+
},
|
396
|
+
});
|
397
|
+
|
398
|
+
export default Parser;
|
package/src/templates.ts
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
import { MDMError, MDMNonParserError } from "./errors";
|
2
|
+
|
1
3
|
const templates: { [key: string]: string } = {};
|
2
4
|
|
3
5
|
/**
|
@@ -6,7 +8,9 @@ const templates: { [key: string]: string } = {};
|
|
6
8
|
* @param content The replacement string
|
7
9
|
*/
|
8
10
|
export function new_template(name: string, content: string) {
|
9
|
-
|
11
|
+
if (name in templates)
|
12
|
+
throw new MDMNonParserError(`Template "${name}" already exists`);
|
13
|
+
templates[name] = content;
|
10
14
|
}
|
11
15
|
|
12
16
|
/* initialize default templates */
|
package/src/types.ts
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
import { HTMLElement } from "node-html-parser";
|
2
|
+
import { Command } from "./commands";
|
3
|
+
|
4
|
+
export type CommandGroupType = {
|
5
|
+
preparse: Command[];
|
6
|
+
parse: Command[];
|
7
|
+
postparse: Command[];
|
8
|
+
};
|
9
|
+
|
10
|
+
type TaggedElementArguments = {
|
11
|
+
repeat?: number;
|
12
|
+
};
|
13
|
+
|
14
|
+
export type TaggedElement = {
|
15
|
+
"html-tag": string;
|
16
|
+
"var-tag": string;
|
17
|
+
_raw: string;
|
18
|
+
args: TaggedElementArguments;
|
19
|
+
node: HTMLElement;
|
20
|
+
};
|
21
|
+
|
22
|
+
export enum CommandType {
|
23
|
+
PREPARSE,
|
24
|
+
PARSE,
|
25
|
+
POSTPARSE,
|
26
|
+
}
|
27
|
+
|
28
|
+
export enum TargetType {
|
29
|
+
HTML,
|
30
|
+
MARKDOWN,
|
31
|
+
}
|
32
|
+
|
33
|
+
export type Checksum = string;
|
@@ -0,0 +1,44 @@
|
|
1
|
+
import fs from "fs";
|
2
|
+
import Parser from "../src/parser";
|
3
|
+
import path from "path";
|
4
|
+
import { expect, jest, test } from "@jest/globals";
|
5
|
+
import { TargetType } from "../src/types";
|
6
|
+
|
7
|
+
beforeAll(() => {
|
8
|
+
return new Promise((res, rej) => {
|
9
|
+
fs.mkdir(path.join("tests", "test-files"), res);
|
10
|
+
});
|
11
|
+
});
|
12
|
+
// afterAll(() => {
|
13
|
+
// return new Promise((res, rej) => {
|
14
|
+
// fs.rm(path.join("tests", "test-files"), { recursive: true }, res);
|
15
|
+
// });
|
16
|
+
// });
|
17
|
+
|
18
|
+
function put(text: string | NodeJS.ArrayBufferView, file: string) {
|
19
|
+
fs.writeFileSync(path.join("tests", "test-files", file), text);
|
20
|
+
}
|
21
|
+
function putDir(name: string) {
|
22
|
+
fs.mkdirSync(path.join("tests", "test-files", name));
|
23
|
+
}
|
24
|
+
|
25
|
+
beforeEach(() => {
|
26
|
+
return new Promise((res, rej) => {
|
27
|
+
fs.mkdir("tests/test-files", res);
|
28
|
+
});
|
29
|
+
});
|
30
|
+
|
31
|
+
afterEach(() => {
|
32
|
+
return new Promise((res, rej) => {
|
33
|
+
fs.rm("tests/test-files", { recursive: true }, res);
|
34
|
+
});
|
35
|
+
});
|
36
|
+
|
37
|
+
export default {
|
38
|
+
expect,
|
39
|
+
path,
|
40
|
+
Parser,
|
41
|
+
put,
|
42
|
+
putDir,
|
43
|
+
TargetType,
|
44
|
+
};
|
@@ -0,0 +1,92 @@
|
|
1
|
+
import util from "./_test-util";
|
2
|
+
|
3
|
+
describe("Use of templates", function () {
|
4
|
+
it("should import presentation template as expected", function () {
|
5
|
+
const output = new util.Parser("#mdtemplate<presentation>").get();
|
6
|
+
const template = `<style>html {width: 100vw;height: 100vh;}.slide {padding: 5%;border-radius: 25px;margin: 0;}div > .slide-num {position: absolute;top: 12.5%;right: 15%;/* font-size: 150%; */}body {margin: 5% 15%;}img {max-width: 100%;max-height: 40vh;}</style><script>document.addEventListener("DOMContentLoaded", () => {let current_slide = 0;const all_slides = document.querySelectorAll("div.slide");const num_slides = all_slides.length;all_slides.forEach((slide) => {const num_elem = document.createElement("p");num_elem.classList.add("slide-num");slide.appendChild(num_elem);});onkeydown = (ev) => {if (ev.key == "ArrowRight" && current_slide < all_slides.length - 1)update_slide(++current_slide);else if (ev.key == "ArrowLeft" && current_slide > 0)update_slide(--current_slide);};const update_slide = (index) => {all_slides.forEach((slide) => (slide.style.display = "none"));all_slides[current_slide].style.display = "block";all_slides[current_slide].lastChild.textContent = \`\${current_slide + 1} / \${num_slides}\`;};update_slide(current_slide);});</script>`;
|
7
|
+
|
8
|
+
util.expect(output).toBe(template + "\n\n");
|
9
|
+
});
|
10
|
+
|
11
|
+
it("should use custom templates from project extensions.js file", () => {
|
12
|
+
util.put(
|
13
|
+
"module.exports = {main: (new_template, _) => {new_template('hi', 'hello');}};",
|
14
|
+
"extensions.js"
|
15
|
+
);
|
16
|
+
util.put("#mdtemplate<hi>", "sample1.md");
|
17
|
+
|
18
|
+
util.expect(new util.Parser("tests/test-files/sample1.md").get()).toBe(
|
19
|
+
"hello\n\n"
|
20
|
+
);
|
21
|
+
});
|
22
|
+
|
23
|
+
it("should use custom commands from project extensions.js file", () => {
|
24
|
+
util.put(
|
25
|
+
'module.exports = {main: (_, new_command) => {new_command(/#teep/, () => "#peet")}};',
|
26
|
+
"extensions.js"
|
27
|
+
);
|
28
|
+
util.put("#teep", "sample1.md");
|
29
|
+
|
30
|
+
const parser = new util.Parser("tests/test-files/sample1.md");
|
31
|
+
const output = parser.get();
|
32
|
+
|
33
|
+
util.expect(output).toEqual("#peet\n\n");
|
34
|
+
});
|
35
|
+
});
|
36
|
+
|
37
|
+
describe("Use of markdown hooks for SSR", () => {
|
38
|
+
it("should allow hooks hooks to be used", () => {
|
39
|
+
util.put("#mdhook<t1>\n<p::var>\n#mdendhook<t1>", "sample1.md");
|
40
|
+
|
41
|
+
const parser = new util.Parser("tests/test-files/sample1.md", {
|
42
|
+
allow_undefined: true,
|
43
|
+
});
|
44
|
+
parser.add_hook("t1", (map) => {
|
45
|
+
map["var"].node.textContent = "complete";
|
46
|
+
});
|
47
|
+
|
48
|
+
const output = parser.get();
|
49
|
+
|
50
|
+
util.expect(output).toBe("<p>complete</p>\n\n");
|
51
|
+
});
|
52
|
+
|
53
|
+
it("should allow for extracting a node from the document as a template using map", () => {
|
54
|
+
util.put(
|
55
|
+
`<html><body>#mdhook<template><b::name><p::class>#mdendhook<template></body></html>`,
|
56
|
+
"sample1.html"
|
57
|
+
);
|
58
|
+
|
59
|
+
const parser = new util.Parser("tests/test-files/sample1.html", {
|
60
|
+
allow_undefined: true,
|
61
|
+
});
|
62
|
+
|
63
|
+
parser.add_hook("template", (map) => {
|
64
|
+
map["name"].node.textContent = "bold";
|
65
|
+
map["class"].node.textContent = "paragraph";
|
66
|
+
});
|
67
|
+
const output = parser.get();
|
68
|
+
|
69
|
+
util.expect(output).toBe(
|
70
|
+
"<html><body><b>bold</b><p>paragraph</p></body></html>\n\n"
|
71
|
+
);
|
72
|
+
});
|
73
|
+
it("should allow for nested hooks to be used", () => {
|
74
|
+
util.put(
|
75
|
+
"#mdhook<t1><p::outer1>#mdhook<t2><p::inner>#mdendhook<t2><p::outer2>#mdendhook<t1>",
|
76
|
+
"sample1.md"
|
77
|
+
);
|
78
|
+
|
79
|
+
const parser = new util.Parser("tests/test-files/sample1.md", {});
|
80
|
+
parser.add_hook("t1", (map) => {
|
81
|
+
map["outer1"].node.textContent = "hello";
|
82
|
+
map["outer2"].node.textContent = "world";
|
83
|
+
});
|
84
|
+
parser.add_hook("t2", (map) => {
|
85
|
+
map["inner"].node.textContent = "!";
|
86
|
+
});
|
87
|
+
|
88
|
+
const output = parser.get();
|
89
|
+
|
90
|
+
util.expect(output).toBe("<p>hello</p><p>!</p><p>world</p>\n\n");
|
91
|
+
});
|
92
|
+
});
|