Path: blob/main/tools/document-generator/main.ts
12924 views
import * as mod from "https://deno.land/std/yaml/mod.ts";12type GeneratorFunction<T> = (context: GeneratorContext) => T;34type Attr = {5id: string;6classes: string[];7attributes: Record<string, string>;8};910type WithAttr = {11attr?: Attr;12};1314type Code = WithAttr & {15type: "Code";16text: string;17};1819type Link = WithAttr & {20type: "Link";21content: Inline[];22target: string;23};2425type Emph = {26type: "Emph";27content: Inline[];28};2930type Str = {31type: "Str";32text: string;33};3435type Space = {36type: "Space";37};3839type Span = WithAttr & {40type: "Span";41content: Inline[];42};4344type Inline = Code | Emph | Str | Space | Span | Shortcode | Link;45const isCode = (inline: Inline): inline is Code => inline.type === "Code";46const isEmph = (inline: Inline): inline is Emph => inline.type === "Emph";47const isStr = (inline: Inline): inline is Str => inline.type === "Str";48const isSpace = (inline: Inline): inline is Space => inline.type === "Space";49const isSpan = (inline: Inline): inline is Span => inline.type === "Span";50const isShortcode = (inline: Inline): inline is Shortcode =>51inline.type === "Shortcode";52const isLink = (inline: Inline): inline is Link => inline.type === "Link";5354type Para = {55type: "Para";56content: Inline[];57};5859type Block = Para;60const isPara = (block: Block): block is Para => block.type === "Para";6162type Document = {63type: "Document";64blocks: Block[];65meta: Record<string, unknown>;66};6768type Shortcode = {69type: "Shortcode";70content: string;71escaped?: boolean;72};7374class RenderContext {75indent: number;76content: string[];7778renderLink(link: Link) {79this.content.push("[");80for (const inline of link.content) {81this.renderInline(inline);82}83this.content.push("]");84this.content.push("(" + link.target + ")");85this.renderAttr(link.attr);86}8788renderAttr(attr?: Attr) {89if (attr === undefined) {90return;91}92this.content.push("{");93this.content.push("#" + attr.id);94for (const className of attr.classes) {95this.content.push(" ." + className);96}97for (const [key, value] of Object.entries(attr.attributes)) {98this.content.push(" " + key + '="' + value + '"');99}100this.content.push("}");101}102103renderSpan(span: Span) {104this.content.push("[");105for (const inline of span.content) {106this.renderInline(inline);107}108this.content.push("]");109if (span.attr) {110this.renderAttr(span.attr);111} else {112this.content.push("{}");113}114}115116renderShortcode(shortcode: Shortcode) {117const open = shortcode.escaped ? "{{{<" : "{{<";118const close = shortcode.escaped ? ">}}}" : ">}}";119this.content.push(`${open} ${shortcode.content} ${close}`);120}121122renderInline(inline: Inline) {123if (isCode(inline)) {124this.content.push("`" + inline.text + "`");125this.renderAttr(inline.attr);126return;127}128if (isEmph(inline)) {129this.content.push("*");130for (const inner of inline.content) {131this.renderInline(inner);132}133this.content.push("*");134return;135}136if (isStr(inline)) {137this.content.push(inline.text);138return;139}140if (isSpace(inline)) {141this.content.push(" ");142return;143}144if (isSpan(inline)) {145this.renderSpan(inline);146}147if (isShortcode(inline)) {148this.renderShortcode(inline);149}150if (isLink(inline)) {151this.renderLink(inline);152}153}154155renderPara(para: Para) {156this.content.push("\n\n");157this.content.push(" ".repeat(this.indent));158// this.indent++;159for (const inline of para.content) {160this.renderInline(inline);161}162// this.indent--;163}164165renderBlock(block: Block) {166if (isPara(block)) {167this.renderPara(block);168return;169}170}171172renderDocument(document: Document) {173if (Object.entries(document.meta).length > 0) {174this.content.push("---\n");175this.content.push(mod.stringify(document.meta));176this.content.push("---\n\n");177}178for (const block of document.blocks) {179this.renderBlock(block);180}181}182183result() {184return this.content.join("");185}186187constructor() {188this.indent = 0;189this.content = [];190}191}192193class GeneratorContext {194probabilities: {195attr: number;196reuseClass: number;197198str: number;199emph: number;200code: number;201span: number;202link: number;203shortcode: number;204targetShortcode: number;205};206207sizes: {208inline: number;209block: number;210sentence: number;211};212213classes: string[];214ids: string[];215216meta: Record<string, unknown>;217218////////////////////////////////////////////////////////////////////////////////219// helpers220221freshId() {222const result = Math.random().toString(36).substr(2232,2243 + (Math.random() * 6),225);226if (result.charCodeAt(0) >= 48 && result.charCodeAt(0) <= 57) {227return "a" + result;228}229return result;230}231232assign(other: GeneratorContext) {233this.probabilities = other.probabilities;234this.sizes = other.sizes;235this.classes = other.classes;236this.ids = other.ids;237this.meta = other.meta;238}239240smaller(): GeneratorContext {241const newContext = new GeneratorContext();242newContext.assign(this);243newContext.sizes = {244...this.sizes,245inline: ~~(this.sizes.inline * 0.5),246block: ~~(this.sizes.block * 0.5),247};248249return newContext;250}251252generatePunctuation() {253const punctuations = [".", "!", "?", ",", ";", ":"];254return punctuations[~~(Math.random() * punctuations.length)];255}256257////////////////////////////////////////////////////////////////////////////////258// Attr-related functions259260randomId() {261const id = this.freshId();262this.ids.push(id);263return id;264}265266randomClass() {267if (268Math.random() < this.probabilities.reuseClass || this.classes.length === 0269) {270const id = this.freshId();271this.classes.push(id);272return id;273} else {274return this.classes[~~(Math.random() * this.classes.length)];275}276}277278randomClasses() {279const classCount = ~~(Math.random() * 3) + 1;280const classes: string[] = [];281for (let i = 0; i < classCount; i++) {282const id = this.randomClass();283// repeat classes across elements but not within the same element284if (classes.indexOf(id) === -1) {285classes.push(id);286}287}288return classes;289}290291randomAttributes() {292const attrCount = ~~(Math.random() * 3) + 1;293const attributes: Record<string, string> = {};294for (let i = 0; i < attrCount; i++) {295attributes[this.freshId()] = this.freshId();296}297return attributes;298}299300randomAttr() {301if (Math.random() >= this.probabilities.attr) {302return undefined;303}304return {305id: this.randomId(),306classes: this.randomClasses(),307attributes: this.randomAttributes(),308};309}310311////////////////////////////////////////////////////////////////////////////////312// Inline-related functions313314chooseInlineType() {315if (Math.random() < this.probabilities.str) {316return "Str";317}318if (Math.random() < this.probabilities.code) {319return "Code";320}321if (Math.random() < this.probabilities.span) {322return "Span";323}324if (Math.random() < this.probabilities.emph) {325return "Emph";326}327if (Math.random() < this.probabilities.link) {328return "Link";329}330if (Math.random() < this.probabilities.shortcode) {331return "InlineShortcode";332}333334return "Null";335}336337generateInlineShortcode(): Shortcode {338const metaKey = this.freshId();339const metaValue = this.freshId();340this.meta[metaKey] = metaValue;341return {342type: "Shortcode",343content: `meta ${metaKey}`,344};345}346347generateStr(): Str {348return {349type: "Str",350text: this.freshId(),351};352}353354generateCode(): Code {355return {356attr: this.randomAttr(),357type: "Code",358text: this.freshId(),359};360}361362generateEmph(): Emph {363const small = this.smaller();364const contentSize = ~~(Math.random() * small.sizes.inline) + 1;365const content: Inline[] = [];366367for (let i = 0; i < contentSize; i++) {368const inline = small.generateInline();369if (inline) {370content.push(inline);371}372}373374return {375type: "Emph",376content,377};378}379380generateSpan(): Span {381const small = this.smaller();382const contentSize = ~~(Math.random() * small.sizes.inline) + 1;383const content: Inline[] = [];384385for (let i = 0; i < contentSize; i++) {386const inline = small.generateInline();387if (inline) {388content.push(inline);389}390}391392return {393attr: this.randomAttr(),394type: "Span",395content,396};397}398399generateTarget(): string {400let target = this.freshId();401if (Math.random() < this.probabilities.targetShortcode) {402const shortcode = this.generateInlineShortcode();403target = `${target}-{{< ${shortcode.content} >}}`;404}405return target;406}407408generateLink(): Link {409const small = this.smaller();410const contentSize = ~~(Math.random() * small.sizes.inline) + 1;411const content: Inline[] = [];412413for (let i = 0; i < contentSize; i++) {414const inline = small.generateInline();415if (inline) {416content.push(inline);417}418}419420return {421attr: this.randomAttr(),422type: "Link",423content,424target: this.generateTarget(),425};426}427428generateInline() {429const dispatch = {430Str: () => this.generateStr(),431Code: () => this.generateCode(),432Emph: () => this.generateEmph(),433Span: () => this.generateSpan(),434Link: () => this.generateLink(),435InlineShortcode: () => this.generateInlineShortcode(),436Null: () => {},437};438return dispatch[this.chooseInlineType()]();439}440441////////////////////////////////////////////////////////////////////////////////442// Block-related functions443444generatePara(): Para {445const small = this.smaller();446const contentSize = ~~(Math.random() * small.sizes.inline) + 1;447const content: Inline[] = [];448449const generateSentence = () => {450const sentenceSize = ~~(Math.random() * small.sizes.sentence) + 1;451452for (let i = 0; i < sentenceSize; i++) {453const inline = small.generateInline();454if (inline) {455content.push(inline);456if (i !== sentenceSize - 1) {457content.push({458type: "Space",459});460} else {461content.push({462type: "Str",463text: small.generatePunctuation(),464});465}466} else {467content.push({468type: "Str",469text: small.generatePunctuation(),470});471}472}473};474475for (let i = 0; i < contentSize; i++) {476generateSentence();477if (i !== contentSize - 1) {478content.push({479type: "Space",480});481}482}483484return {485type: "Para",486content,487};488}489490generateBlock(): Block {491return this.generatePara();492}493494////////////////////////////////////////////////////////////////////////////////495// Document-related functions496497generateDocument(): Document {498const small = this.smaller();499const blockSize = ~~(Math.random() * small.sizes.block) + 1;500const blocks: Block[] = [];501502for (let i = 0; i < blockSize; i++) {503blocks.push(small.generateBlock());504}505506const result: Document = {507type: "Document",508blocks,509meta: this.meta,510};511return result;512}513514////////////////////////////////////////////////////////////////////////////////515516constructor() {517this.classes = [];518this.ids = [];519this.probabilities = {520attr: 0.95,521reuseClass: 0.5,522523str: 0.9,524code: 0.5,525span: 0.5,526emph: 0.5,527link: 0.5,528shortcode: 0.5,529targetShortcode: 0.25,530};531this.sizes = {532inline: 10,533block: 10,534sentence: 10,535};536this.meta = {};537}538}539540const doc = new GeneratorContext().generateDocument();541const renderer = new RenderContext();542renderer.renderDocument(doc);543544// console.log(JSON.stringify(doc, null, 2));545console.log(renderer.result());546547548