Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
quarto-dev
GitHub Repository: quarto-dev/quarto-cli
Path: blob/main/tools/document-generator/main.ts
3557 views
1
import * as mod from "https://deno.land/std/yaml/mod.ts";
2
3
type GeneratorFunction<T> = (context: GeneratorContext) => T;
4
5
type Attr = {
6
id: string;
7
classes: string[];
8
attributes: Record<string, string>;
9
};
10
11
type WithAttr = {
12
attr?: Attr;
13
};
14
15
type Code = WithAttr & {
16
type: "Code";
17
text: string;
18
};
19
20
type Link = WithAttr & {
21
type: "Link";
22
content: Inline[];
23
target: string;
24
};
25
26
type Emph = {
27
type: "Emph";
28
content: Inline[];
29
};
30
31
type Str = {
32
type: "Str";
33
text: string;
34
};
35
36
type Space = {
37
type: "Space";
38
};
39
40
type Span = WithAttr & {
41
type: "Span";
42
content: Inline[];
43
};
44
45
type Inline = Code | Emph | Str | Space | Span | Shortcode | Link;
46
const isCode = (inline: Inline): inline is Code => inline.type === "Code";
47
const isEmph = (inline: Inline): inline is Emph => inline.type === "Emph";
48
const isStr = (inline: Inline): inline is Str => inline.type === "Str";
49
const isSpace = (inline: Inline): inline is Space => inline.type === "Space";
50
const isSpan = (inline: Inline): inline is Span => inline.type === "Span";
51
const isShortcode = (inline: Inline): inline is Shortcode =>
52
inline.type === "Shortcode";
53
const isLink = (inline: Inline): inline is Link => inline.type === "Link";
54
55
type Para = {
56
type: "Para";
57
content: Inline[];
58
};
59
60
type Block = Para;
61
const isPara = (block: Block): block is Para => block.type === "Para";
62
63
type Document = {
64
type: "Document";
65
blocks: Block[];
66
meta: Record<string, unknown>;
67
};
68
69
type Shortcode = {
70
type: "Shortcode";
71
content: string;
72
escaped?: boolean;
73
};
74
75
class RenderContext {
76
indent: number;
77
content: string[];
78
79
renderLink(link: Link) {
80
this.content.push("[");
81
for (const inline of link.content) {
82
this.renderInline(inline);
83
}
84
this.content.push("]");
85
this.content.push("(" + link.target + ")");
86
this.renderAttr(link.attr);
87
}
88
89
renderAttr(attr?: Attr) {
90
if (attr === undefined) {
91
return;
92
}
93
this.content.push("{");
94
this.content.push("#" + attr.id);
95
for (const className of attr.classes) {
96
this.content.push(" ." + className);
97
}
98
for (const [key, value] of Object.entries(attr.attributes)) {
99
this.content.push(" " + key + '="' + value + '"');
100
}
101
this.content.push("}");
102
}
103
104
renderSpan(span: Span) {
105
this.content.push("[");
106
for (const inline of span.content) {
107
this.renderInline(inline);
108
}
109
this.content.push("]");
110
if (span.attr) {
111
this.renderAttr(span.attr);
112
} else {
113
this.content.push("{}");
114
}
115
}
116
117
renderShortcode(shortcode: Shortcode) {
118
const open = shortcode.escaped ? "{{{<" : "{{<";
119
const close = shortcode.escaped ? ">}}}" : ">}}";
120
this.content.push(`${open} ${shortcode.content} ${close}`);
121
}
122
123
renderInline(inline: Inline) {
124
if (isCode(inline)) {
125
this.content.push("`" + inline.text + "`");
126
this.renderAttr(inline.attr);
127
return;
128
}
129
if (isEmph(inline)) {
130
this.content.push("*");
131
for (const inner of inline.content) {
132
this.renderInline(inner);
133
}
134
this.content.push("*");
135
return;
136
}
137
if (isStr(inline)) {
138
this.content.push(inline.text);
139
return;
140
}
141
if (isSpace(inline)) {
142
this.content.push(" ");
143
return;
144
}
145
if (isSpan(inline)) {
146
this.renderSpan(inline);
147
}
148
if (isShortcode(inline)) {
149
this.renderShortcode(inline);
150
}
151
if (isLink(inline)) {
152
this.renderLink(inline);
153
}
154
}
155
156
renderPara(para: Para) {
157
this.content.push("\n\n");
158
this.content.push(" ".repeat(this.indent));
159
// this.indent++;
160
for (const inline of para.content) {
161
this.renderInline(inline);
162
}
163
// this.indent--;
164
}
165
166
renderBlock(block: Block) {
167
if (isPara(block)) {
168
this.renderPara(block);
169
return;
170
}
171
}
172
173
renderDocument(document: Document) {
174
if (Object.entries(document.meta).length > 0) {
175
this.content.push("---\n");
176
this.content.push(mod.stringify(document.meta));
177
this.content.push("---\n\n");
178
}
179
for (const block of document.blocks) {
180
this.renderBlock(block);
181
}
182
}
183
184
result() {
185
return this.content.join("");
186
}
187
188
constructor() {
189
this.indent = 0;
190
this.content = [];
191
}
192
}
193
194
class GeneratorContext {
195
probabilities: {
196
attr: number;
197
reuseClass: number;
198
199
str: number;
200
emph: number;
201
code: number;
202
span: number;
203
link: number;
204
shortcode: number;
205
targetShortcode: number;
206
};
207
208
sizes: {
209
inline: number;
210
block: number;
211
sentence: number;
212
};
213
214
classes: string[];
215
ids: string[];
216
217
meta: Record<string, unknown>;
218
219
////////////////////////////////////////////////////////////////////////////////
220
// helpers
221
222
freshId() {
223
const result = Math.random().toString(36).substr(
224
2,
225
3 + (Math.random() * 6),
226
);
227
if (result.charCodeAt(0) >= 48 && result.charCodeAt(0) <= 57) {
228
return "a" + result;
229
}
230
return result;
231
}
232
233
assign(other: GeneratorContext) {
234
this.probabilities = other.probabilities;
235
this.sizes = other.sizes;
236
this.classes = other.classes;
237
this.ids = other.ids;
238
this.meta = other.meta;
239
}
240
241
smaller(): GeneratorContext {
242
const newContext = new GeneratorContext();
243
newContext.assign(this);
244
newContext.sizes = {
245
...this.sizes,
246
inline: ~~(this.sizes.inline * 0.5),
247
block: ~~(this.sizes.block * 0.5),
248
};
249
250
return newContext;
251
}
252
253
generatePunctuation() {
254
const punctuations = [".", "!", "?", ",", ";", ":"];
255
return punctuations[~~(Math.random() * punctuations.length)];
256
}
257
258
////////////////////////////////////////////////////////////////////////////////
259
// Attr-related functions
260
261
randomId() {
262
const id = this.freshId();
263
this.ids.push(id);
264
return id;
265
}
266
267
randomClass() {
268
if (
269
Math.random() < this.probabilities.reuseClass || this.classes.length === 0
270
) {
271
const id = this.freshId();
272
this.classes.push(id);
273
return id;
274
} else {
275
return this.classes[~~(Math.random() * this.classes.length)];
276
}
277
}
278
279
randomClasses() {
280
const classCount = ~~(Math.random() * 3) + 1;
281
const classes: string[] = [];
282
for (let i = 0; i < classCount; i++) {
283
const id = this.randomClass();
284
// repeat classes across elements but not within the same element
285
if (classes.indexOf(id) === -1) {
286
classes.push(id);
287
}
288
}
289
return classes;
290
}
291
292
randomAttributes() {
293
const attrCount = ~~(Math.random() * 3) + 1;
294
const attributes: Record<string, string> = {};
295
for (let i = 0; i < attrCount; i++) {
296
attributes[this.freshId()] = this.freshId();
297
}
298
return attributes;
299
}
300
301
randomAttr() {
302
if (Math.random() >= this.probabilities.attr) {
303
return undefined;
304
}
305
return {
306
id: this.randomId(),
307
classes: this.randomClasses(),
308
attributes: this.randomAttributes(),
309
};
310
}
311
312
////////////////////////////////////////////////////////////////////////////////
313
// Inline-related functions
314
315
chooseInlineType() {
316
if (Math.random() < this.probabilities.str) {
317
return "Str";
318
}
319
if (Math.random() < this.probabilities.code) {
320
return "Code";
321
}
322
if (Math.random() < this.probabilities.span) {
323
return "Span";
324
}
325
if (Math.random() < this.probabilities.emph) {
326
return "Emph";
327
}
328
if (Math.random() < this.probabilities.link) {
329
return "Link";
330
}
331
if (Math.random() < this.probabilities.shortcode) {
332
return "InlineShortcode";
333
}
334
335
return "Null";
336
}
337
338
generateInlineShortcode(): Shortcode {
339
const metaKey = this.freshId();
340
const metaValue = this.freshId();
341
this.meta[metaKey] = metaValue;
342
return {
343
type: "Shortcode",
344
content: `meta ${metaKey}`,
345
};
346
}
347
348
generateStr(): Str {
349
return {
350
type: "Str",
351
text: this.freshId(),
352
};
353
}
354
355
generateCode(): Code {
356
return {
357
attr: this.randomAttr(),
358
type: "Code",
359
text: this.freshId(),
360
};
361
}
362
363
generateEmph(): Emph {
364
const small = this.smaller();
365
const contentSize = ~~(Math.random() * small.sizes.inline) + 1;
366
const content: Inline[] = [];
367
368
for (let i = 0; i < contentSize; i++) {
369
const inline = small.generateInline();
370
if (inline) {
371
content.push(inline);
372
}
373
}
374
375
return {
376
type: "Emph",
377
content,
378
};
379
}
380
381
generateSpan(): Span {
382
const small = this.smaller();
383
const contentSize = ~~(Math.random() * small.sizes.inline) + 1;
384
const content: Inline[] = [];
385
386
for (let i = 0; i < contentSize; i++) {
387
const inline = small.generateInline();
388
if (inline) {
389
content.push(inline);
390
}
391
}
392
393
return {
394
attr: this.randomAttr(),
395
type: "Span",
396
content,
397
};
398
}
399
400
generateTarget(): string {
401
let target = this.freshId();
402
if (Math.random() < this.probabilities.targetShortcode) {
403
const shortcode = this.generateInlineShortcode();
404
target = `${target}-{{< ${shortcode.content} >}}`;
405
}
406
return target;
407
}
408
409
generateLink(): Link {
410
const small = this.smaller();
411
const contentSize = ~~(Math.random() * small.sizes.inline) + 1;
412
const content: Inline[] = [];
413
414
for (let i = 0; i < contentSize; i++) {
415
const inline = small.generateInline();
416
if (inline) {
417
content.push(inline);
418
}
419
}
420
421
return {
422
attr: this.randomAttr(),
423
type: "Link",
424
content,
425
target: this.generateTarget(),
426
};
427
}
428
429
generateInline() {
430
const dispatch = {
431
Str: () => this.generateStr(),
432
Code: () => this.generateCode(),
433
Emph: () => this.generateEmph(),
434
Span: () => this.generateSpan(),
435
Link: () => this.generateLink(),
436
InlineShortcode: () => this.generateInlineShortcode(),
437
Null: () => {},
438
};
439
return dispatch[this.chooseInlineType()]();
440
}
441
442
////////////////////////////////////////////////////////////////////////////////
443
// Block-related functions
444
445
generatePara(): Para {
446
const small = this.smaller();
447
const contentSize = ~~(Math.random() * small.sizes.inline) + 1;
448
const content: Inline[] = [];
449
450
const generateSentence = () => {
451
const sentenceSize = ~~(Math.random() * small.sizes.sentence) + 1;
452
453
for (let i = 0; i < sentenceSize; i++) {
454
const inline = small.generateInline();
455
if (inline) {
456
content.push(inline);
457
if (i !== sentenceSize - 1) {
458
content.push({
459
type: "Space",
460
});
461
} else {
462
content.push({
463
type: "Str",
464
text: small.generatePunctuation(),
465
});
466
}
467
} else {
468
content.push({
469
type: "Str",
470
text: small.generatePunctuation(),
471
});
472
}
473
}
474
};
475
476
for (let i = 0; i < contentSize; i++) {
477
generateSentence();
478
if (i !== contentSize - 1) {
479
content.push({
480
type: "Space",
481
});
482
}
483
}
484
485
return {
486
type: "Para",
487
content,
488
};
489
}
490
491
generateBlock(): Block {
492
return this.generatePara();
493
}
494
495
////////////////////////////////////////////////////////////////////////////////
496
// Document-related functions
497
498
generateDocument(): Document {
499
const small = this.smaller();
500
const blockSize = ~~(Math.random() * small.sizes.block) + 1;
501
const blocks: Block[] = [];
502
503
for (let i = 0; i < blockSize; i++) {
504
blocks.push(small.generateBlock());
505
}
506
507
const result: Document = {
508
type: "Document",
509
blocks,
510
meta: this.meta,
511
};
512
return result;
513
}
514
515
////////////////////////////////////////////////////////////////////////////////
516
517
constructor() {
518
this.classes = [];
519
this.ids = [];
520
this.probabilities = {
521
attr: 0.95,
522
reuseClass: 0.5,
523
524
str: 0.9,
525
code: 0.5,
526
span: 0.5,
527
emph: 0.5,
528
link: 0.5,
529
shortcode: 0.5,
530
targetShortcode: 0.25,
531
};
532
this.sizes = {
533
inline: 10,
534
block: 10,
535
sentence: 10,
536
};
537
this.meta = {};
538
}
539
}
540
541
const doc = new GeneratorContext().generateDocument();
542
const renderer = new RenderContext();
543
renderer.renderDocument(doc);
544
545
// console.log(JSON.stringify(doc, null, 2));
546
console.log(renderer.result());
547
548