@longform/longform 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../lib/longform.ts"],
4
+ "sourcesContent": ["import type { ChunkType, FragmentType, ParsedResult, WorkingChunk, WorkingElement, WorkingFragment, Fragment } from \"./types.ts\";\n\nexport type {\n FragmentType,\n Fragment,\n ParsedResult\n};\n\nconst sniffTestRe = /^(?:(?:(--).*)|(?: *(@|#).*)|(?: *[\\w\\-]+(?::[\\w\\-]+)?(?:[#.[][^\\n]+)?(::).*)|(?: +([\\[\"]).*)|(\\ \\ .*))$/gmi\n , element1 = /((?:\\ \\ )+)? ?([\\w\\-]+(?::[\\w\\-]+)?)([#\\.\\[][^\\n]*)?::(?: ({{?|[^\\n]+))?/gmi\n , directive1 = /((?:\\ \\ )+)? ?@([\\w][\\w\\-]+)(?::: ?([^\\n]+)?)?/gmi\n , attribute1 = /((?:\\ \\ )+)\\[(\\w[\\w-]*(?::\\w[\\w-]*)?)(?:=([^\\n]+))?\\]/\n , preformattedClose = /[ \\t]*}}?[ \\t]*/\n , id1 = /((?:\\ \\ )+)?#(#)?([\\w\\-]+)(?: ([\\[\"]))?/gmi\n , idnt1 = /^(\\ \\ )+/\n , text1 = /^((?:\\ \\ )+)([^ \\n][^\\n]*)$/i\n , paramsRe = /(?:(#|\\.)([^#.\\[\\n]+)|(?:\\[(\\w[\\w\\-]*(?::\\w[\\w\\-]*)?)(?:=([^\\n\\]]+))?\\]))/g\n , refRe = /#\\[([\\w\\-]+)\\]/g\n , escapeRe = /([&<>\"'#\\[\\]{}])/g\n , templateLinesRe = /^(\\ \\ )?([^\\n]*)$/gmi\n , voids = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wrb']);\n\nlet m1: RegExpExecArray | null\n , m2: RegExpExecArray | null\n , m3: RegExpExecArray | null;\n\nconst entities = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&apos;',\n// '#': '&num;',\n// '[': '&lbrak;',\n// ']': '&rbrak;',\n// '{': '&rbrace;',\n// '}': '&lbrace;',\n};\n\nfunction escape(value: string): string {\n return value.replace(escapeRe, (match) => {\n return entities[match] ?? match;\n });\n}\n\nfunction makeElement(indent: number = 0): WorkingElement {\n return {\n indent,\n html: '',\n attrs: {},\n };\n}\n\nfunction makeChunk(type: ChunkType = 'parsed'): WorkingChunk {\n return {\n type,\n html: '',\n els: [],\n };\n}\n\nfunction makeFragment(type: FragmentType = 'bare'): WorkingFragment {\n return {\n type,\n html: '',\n template: false,\n els: [],\n chunks: [],\n refs: [],\n };\n}\n\n/**\n * Parses a longform document into a object containing the root and fragments\n * in the output format.\n *\n * @param {string} doc - The longform document to parse.\n * @returns {ParsedResult}\n */\nexport function longform(doc: string, debug: (...d: unknown[]) => void = () => {}): ParsedResult {\n let skipping: boolean = false\n , textIndent: number | null = null\n , verbatimSerialize: boolean = true\n , verbatimIndent: number | null = null\n , verbatimFirst: boolean = false\n , element: WorkingElement = makeElement()\n , chunk: WorkingChunk | null = makeChunk()\n , fragment: WorkingFragment = makeFragment()\n // the root fragment\n , root: WorkingFragment | null = null\n // ids of claimed fragments\n const claimed: Set<string> = new Set()\n // parsed fragments\n , parsed: Map<string, WorkingFragment> = new Map()\n , output: ParsedResult = Object.create(null);\n\n output.fragments = Object.create(null);\n output.templates = Object.create(null);\n \n \n /**\n * Closes any current in progress element definition\n * and creates a new working element.\n */\n function applyIndent(targetIndent: number) {\n if (element.tag != null) {\n const root = fragment.type === 'range'\n ? targetIndent < 2\n : fragment.html === ''\n ;\n\n fragment.html += `<${element.tag}`\n\n if (root) {\n if (fragment.type === 'root') {\n fragment.html += ` data-lf-root`;\n } else if (fragment.type === 'bare' || fragment.type === 'range') {\n fragment.html += ` data-lf=\"${fragment.id}\"`;\n }\n }\n\n if (element.id != null) {\n fragment.html += ' id=\"' + element.id + '\"';\n }\n\n if (element.class != null) {\n fragment.html += ' class=\"' + element.class + '\"';\n }\n\n for (const attr of Object.entries(element.attrs)) {\n if (attr[1] == null) {\n fragment.html += ' ' + attr[0]\n } else {\n fragment.html += ` ${attr[0]}=\"${attr[1]}\"`;\n }\n }\n\n fragment.html += '>';\n\n if (!voids.has(element.tag as string) && element.text != null) {\n fragment.html += element.text;\n }\n\n if (\n !voids.has(element.tag as string)\n ) {\n fragment.els.push(element);\n }\n }\n\n if (targetIndent <= element.indent) {\n element = makeElement(targetIndent);\n\n while (\n fragment.els.length !== 0 && (\n targetIndent == null ||\n fragment.els[fragment.els.length - 1].indent !== targetIndent - 1\n )\n ) {\n const element = fragment.els.pop();\n\n fragment.html += `</${element?.tag}>`;\n }\n\n if (targetIndent === 0) {\n debug(0, '<', fragment.type, fragment.id);\n if (fragment.template) {\n output.templates[fragment.id] = fragment.html;\n } else if (fragment.type === 'root') {\n root = fragment;\n } else {\n parsed.set(fragment.id, fragment);\n }\n\n fragment = makeFragment();\n }\n } else {\n element = makeElement(targetIndent)\n }\n }\n\n while ((m1 = sniffTestRe.exec(doc))) {\n if (m1[1] === '--') {\n continue;\n } else if (fragment.template) {\n fragment.html += m1[0];\n }\n\n // If this is a script tag or preformatted block\n // we want to retain the intended formatting less\n // the indent. Preformatting can apply to any element\n // by ending the declaration with `:: {`.\n if (verbatimIndent != null) {\n // inside a script or preformatted block\n idnt1.lastIndex = 0;\n m2 = idnt1.exec(m1[0]);\n const indent = m2 == null\n ? null\n : m2[0].length / 2;\n\n if (m2 == null || indent as number <= verbatimIndent) {\n fragment.html += '\\n';\n debug(indent, '}', m2?.[0]);\n\n applyIndent(indent);\n verbatimIndent = null;\n verbatimFirst = false;\n textIndent = indent;\n\n if (preformattedClose.test(m1[0])) {\n continue;\n }\n } else {\n const line = m1[0].replace(' '.repeat(verbatimIndent + 1), '');\n debug(indent, '{', line);\n\n if (element.tag != null) {\n applyIndent(indent as number);\n }\n\n if (verbatimFirst) {\n verbatimFirst = false;\n } else {\n fragment.html += '\\n';\n }\n \n if (verbatimSerialize) {\n fragment.html += escape(line);\n } else {\n fragment.html += line;\n }\n\n continue;\n }\n }\n\n if (m1[0].trim() === '') {\n continue;\n }\n\n switch (m1[2] ?? m1[3] ?? m1[4]) {\n // deno-lint-ignore no-fallthrough\n case '#': {\n id1.lastIndex = 0;\n m2 = id1.exec(m1[0]);\n\n if (m2 != null) {\n const indent = (m2[1]?.length ?? 0) / 2;\n\n if (element.tag != null || textIndent != null) {\n applyIndent(indent);\n textIndent = null;\n }\n\n debug(indent, 'id', m2[2], m2[3], m2[4]);\n\n fragment.id = m2[3];\n\n if (indent === 0) {\n if (m2[4] == '[') {\n fragment.type = 'range';\n } else if (m2[4] === '\"') {\n fragment.type = 'text';\n } else if (m2[2] != null) {\n fragment.type = 'bare';\n } else {\n fragment.type = 'embed';\n element.id = fragment.id;\n }\n }\n\n break;\n }\n }\n case '@':\n case '[':\n // deno-lint-ignore no-fallthrough\n case '::': {\n element1.lastIndex = 0;\n // fall through if m1[3] is a # or @\n m2 = m1[2] ?? m1[4] != null\n ? null\n : element1.exec(m1[0]);\n\n // if null then invalid element selector\n // allow the default text case to handle\n if (m2 != null) {\n const indent = (m2[1]?.length ?? 0) / 2\n , tg = m2[2]\n , ar = m2[3]\n , pr = m2[4] === '{' || m2[4] === '{{'\n const tx = pr ? null : m2[4]\n\n debug(indent, 'e', tg, pr, tx);\n\n if (\n element.tag != null ||\n element.indent > indent\n ) {\n applyIndent(indent);\n }\n\n element.indent = indent;\n element.tag = tg;\n\n textIndent = null;\n \n if (indent === 0 && fragment.id == null) {\n if (root != null) {\n skipping = true;\n } else {\n fragment.type = 'root';\n root = fragment;\n }\n }\n \n if (ar != null) {\n debug(indent, 'a', ar);\n while ((m2 = paramsRe.exec(ar))) {\n if (m2[1] === '#') {\n element.id = m2[2];\n } else if (m2[1] === '.') {\n if (element.class == null) {\n element.class = m2[2];\n } else {\n element.class += ' ' + m2[2];\n }\n } else {\n if (m2[3] === 'id') {\n if (element.id == null) {\n element.id = m2[4];\n }\n } else if (m2[3] === 'class') {\n if (element.class == null) {\n element.class = m2[4]\n } else {\n element.class += ' ' + m2[4]\n }\n } else {\n element.attrs[m2[3]] = m2[4];\n }\n }\n }\n }\n\n if (!pr && tx != null) {\n element.text = tx;\n } else if (pr) {\n verbatimFirst = true;\n verbatimIndent = indent;\n verbatimSerialize = m2[4] === '{';\n }\n\n break;\n }\n\n attribute1.lastIndex = 0;\n m2 = m1[2] != null\n ? null\n : attribute1.exec(m1[0]);\n\n if (m2 != null && element.tag != null) {\n debug('a', m2[2], m2[3]);\n\n if (m2[2] === 'id') {\n if (element.id == null) {\n element.id = m2[3].trim();\n }\n } else if (m2[2] === 'class') {\n if (element.class != null) {\n element.class += ' ' + m2[3].trim();\n } else {\n element.class = m2[3].trim();\n }\n } else if (element.attrs[m2[2]] != null) {\n element.attrs[m2[2]] += m2[3];\n } else {\n element.attrs[m2[2]] = m2[3];\n }\n\n break;\n }\n\n directive1.lastIndex = 0;\n m2 = m1[3] != null\n ? null \n : directive1.exec(m1[0]);\n\n if (m2 != null) {\n const indent = (m2[1]?.length ?? 0) / 2;\n\n if (element.tag != null || textIndent != null) {\n applyIndent(indent);\n }\n\n debug(indent, 'd', m2[2], m2[3]);\n\n switch (m2[2]) {\n case 'doctype': {\n fragment.html += `<!doctype ${m2[3] ?? 'html'}>`;\n break;\n }\n case 'xml': {\n fragment.html += `<?xml ${m2[3] ?? 'version=\"1.0\" encoding=\"UTF-8\"'}?>`;\n break;\n }\n case 'template': {\n let indented = false;\n fragment.template = indent === 0;\n\n templateLinesRe.lastIndex = sniffTestRe.lastIndex;\n while ((m2 = templateLinesRe.exec(doc))) {\n if (m2[1] == null && !indented) {\n id1.lastIndex = 0;\n m3 = id1.exec(m2[0]);\n\n fragment.id = m3[3];\n fragment.html += m2[0];\n } else if (m2[1] == null && indented) {\n sniffTestRe.lastIndex = templateLinesRe.lastIndex - 1;\n applyIndent(0)\n break;\n } else {\n fragment.html += '\\n' + m2[0];\n }\n indented = true;\n }\n }\n }\n\n break;\n }\n\n }\n default: {\n m2 = text1.exec(m1[0]) as RegExpExecArray;\n\n if (m2 == null) {\n break;\n }\n const indent = m2[1].length / 2;\n const tx = m2[2].trim();\n\n debug(indent, 't', m2[2]);\n\n if (element.tag != null) {\n applyIndent(indent);\n\n fragment.html += tx;\n } else if (fragment.type === 'text' && fragment.html === '') {\n fragment.html += tx;\n } else {\n fragment.html += ' ' + tx;\n }\n\n textIndent = indent;\n\n while ((m2 = refRe.exec(tx))) {\n const start = fragment.html.length + m2.index - tx.length;\n\n fragment.refs.push({\n id: m2[1],\n start,\n end: start + m2[0].length,\n });\n }\n\n break;\n }\n }\n }\n\n applyIndent(0);\n\n const arr = Array.from(parsed.values());\n\n function flatten(fragment: WorkingFragment): WorkingFragment {\n // work backwards so we don't change the html string length\n // for the later replacements\n for (let j = fragment.refs.length - 1; j >= 0; j--) {\n const ref = fragment.refs[j];\n\n if (claimed.has(ref.id) || !parsed.has(ref.id)) {\n fragment.html = fragment.html.slice(0, ref.start)\n + fragment.html.slice(ref.end)\n } else {\n const child = flatten(parsed.get(ref.id));\n\n fragment.html = fragment.html.slice(0, ref.start)\n + child.html\n + fragment.html.slice(ref.end);\n\n if (child.type === 'embed') {\n claimed.add(child.id)\n }\n }\n }\n\n fragment.refs = [];\n\n return fragment;\n }\n\n for (let i = 0; i < parsed.size + 1; i++) {\n let fragment: WorkingFragment;\n \n if (i === 0 && root == null) {\n continue;\n } else if (i === 0) {\n fragment = root;\n } else {\n fragment = arr[i - 1];\n }\n\n if (fragment.refs.length === 0) {\n continue;\n }\n\n flatten(fragment)\n }\n\n if (root?.html != null) {\n output.root = root.html;\n output.selector = `[data-lf-root]`;\n }\n\n for (let i = 0; i < arr.length; i++) {\n let selector: string;\n const fragment = arr[i];\n\n if (fragment == null || claimed.has(fragment.id)) {\n continue;\n }\n\n if (fragment.type === 'embed') {\n selector = `[id=${fragment.id}]`;\n } else if (fragment.type === 'bare') {\n selector = `[data-lf=${fragment.id}]`;\n } else if (fragment.type === 'range') {\n selector = `[data-lf=${fragment.id}]`;\n }\n\n output.fragments[fragment.id] = {\n id: fragment.id,\n selector,\n type: fragment.type as 'embed' | 'bare' | 'range',\n html: fragment.html,\n };\n }\n\n return output;\n}\n\n\nconst templateRe = /(?:#{([\\w][\\w\\-_]*)})|(?:#\\[([\\w][\\w\\-_]+)\\])/g;\n\n/**\n * Processes a client side Longform template to HTML fragment string.\n *\n * @param fragment - The fragment identifier.\n * @param args - A record of template arguments.\n * @param getFragment - A function which returns an already processed fragment's HTML string.\n * @returns The processed template.\n */\nexport function processTemplate(\n template: string,\n args: Record<string, string | number>,\n getFragment: (fragment: string) => string | undefined,\n): string | undefined {\n const lf = template.replace(templateRe, (_match, param, ref) => {\n if (ref != null) {\n const fragment = getFragment(ref);\n\n if (fragment == null) return '';\n\n return fragment;\n }\n \n return args[param] != null ? escape(args[param].toString()) : '';\n });\n\n return Object.values(longform(lf).fragments)[0]?.html ?? null;\n}\n"],
5
+ "mappings": "AAQA,MAAMA,EAAc,+GAChBC,EAAW,8EACXC,EAAa,oDACbC,EAAa,wDACbC,EAAoB,kBACpBC,EAAM,6CACNC,EAAQ,WACRC,EAAQ,+BACRC,EAAW,6EACXC,EAAQ,kBACRC,EAAW,oBACXC,EAAkB,uBAClBC,EAAQ,IAAI,IAAI,CAAC,OAAQ,OAAQ,KAAM,MAAO,QAAS,KAAM,MAAO,QAAS,OAAQ,OAAQ,QAAS,SAAU,QAAS,KAAK,CAAC,EAEnI,IAAIC,EACAC,EACAC,EAEJ,MAAMC,EAAW,CACf,IAAK,QACL,IAAK,OACL,IAAK,OACL,IAAK,SACL,IAAK,QAMP,EAEA,SAASC,EAAOC,EAAuB,CACrC,OAAOA,EAAM,QAAQR,EAAWS,GAAU,CAxC5C,IAAAC,EAyCI,OAAOA,EAAAJ,EAASG,CAAK,IAAd,KAAAC,EAAmBD,CAC5B,CAAC,CACH,CAEA,SAASE,EAAYC,EAAiB,EAAmB,CACvD,MAAO,CACL,OAAAA,EACA,KAAM,GACN,MAAO,CAAC,CACV,CACF,CAEA,SAASC,EAAUC,EAAkB,SAAwB,CAC3D,MAAO,CACL,KAAAA,EACA,KAAM,GACN,IAAK,CAAC,CACR,CACF,CAEA,SAASC,EAAaD,EAAqB,OAAyB,CAClE,MAAO,CACL,KAAAA,EACA,KAAM,GACN,SAAU,GACV,IAAK,CAAC,EACN,OAAQ,CAAC,EACT,KAAM,CAAC,CACT,CACF,CASO,gBAAS,SAASE,EAAaC,EAAmC,IAAM,CAAC,EAAiB,CA/EjG,IAAAP,EAAAQ,EAAAC,EAAAC,EAAAC,EAAAC,EAAAC,EAAAC,EAAAC,EAAAC,EAAAC,EAgFE,IAAIC,EAAoB,GACpBC,EAA4B,KAC5BC,EAA6B,GAC7BC,EAAgC,KAChCC,EAAyB,GACzBC,EAA0BtB,EAAY,EACtCuB,EAA6BrB,EAAU,EACvCsB,EAA4BpB,EAAa,EAEzCqB,EAA+B,KAEnC,MAAMC,EAAuB,IAAI,IAE7BC,EAAuC,IAAI,IAC3CC,EAAuB,OAAO,OAAO,IAAI,EAE7CA,EAAO,UAAY,OAAO,OAAO,IAAI,EACrCA,EAAO,UAAY,OAAO,OAAO,IAAI,EAOrC,SAASC,EAAYC,EAAsB,CACzC,GAAIR,EAAQ,KAAO,KAAM,CACvB,MAAMG,EAAOD,EAAS,OAAS,QAC3BM,EAAe,EACfN,EAAS,OAAS,GAGtBA,EAAS,MAAQ,IAAIF,EAAQ,GAAG,GAE5BG,IACED,EAAS,OAAS,OACpBA,EAAS,MAAQ,iBACRA,EAAS,OAAS,QAAUA,EAAS,OAAS,WACvDA,EAAS,MAAQ,aAAaA,EAAS,EAAE,MAIzCF,EAAQ,IAAM,OAChBE,EAAS,MAAQ,QAAUF,EAAQ,GAAK,KAGtCA,EAAQ,OAAS,OACnBE,EAAS,MAAQ,WAAaF,EAAQ,MAAQ,KAGhD,UAAWS,KAAQ,OAAO,QAAQT,EAAQ,KAAK,EACzCS,EAAK,CAAC,GAAK,KACbP,EAAS,MAAQ,IAAMO,EAAK,CAAC,EAE7BP,EAAS,MAAQ,IAAIO,EAAK,CAAC,CAAC,KAAKA,EAAK,CAAC,CAAC,IAI5CP,EAAS,MAAQ,IAEb,CAACjC,EAAM,IAAI+B,EAAQ,GAAa,GAAKA,EAAQ,MAAQ,OACvDE,EAAS,MAAQF,EAAQ,MAIxB/B,EAAM,IAAI+B,EAAQ,GAAa,GAEhCE,EAAS,IAAI,KAAKF,CAAO,CAE7B,CAEA,GAAIQ,GAAgBR,EAAQ,OAAQ,CAGlC,IAFAA,EAAUtB,EAAY8B,CAAY,EAGhCN,EAAS,IAAI,SAAW,IACtBM,GAAgB,MAChBN,EAAS,IAAIA,EAAS,IAAI,OAAS,CAAC,EAAE,SAAWM,EAAe,IAElE,CACA,MAAMR,EAAUE,EAAS,IAAI,IAAI,EAEjCA,EAAS,MAAQ,KAAKF,GAAA,YAAAA,EAAS,GAAG,GACpC,CAEIQ,IAAiB,IACnBxB,EAAM,EAAG,IAAKkB,EAAS,KAAMA,EAAS,EAAE,EACpCA,EAAS,SACXI,EAAO,UAAUJ,EAAS,EAAE,EAAIA,EAAS,KAChCA,EAAS,OAAS,OAC3BC,EAAOD,EAEPG,EAAO,IAAIH,EAAS,GAAIA,CAAQ,EAGlCA,EAAWpB,EAAa,EAE5B,MACEkB,EAAUtB,EAAY8B,CAAY,CAEtC,CAEA,KAAQtC,EAAKb,EAAY,KAAK0B,CAAG,GAC/B,GAAIb,EAAG,CAAC,IAAM,KAUd,IARWgC,EAAS,WAClBA,EAAS,MAAQhC,EAAG,CAAC,GAOnB4B,GAAkB,KAAM,CAE1BnC,EAAM,UAAY,EAClBQ,EAAKR,EAAM,KAAKO,EAAG,CAAC,CAAC,EACrB,MAAMS,EAASR,GAAM,KACjB,KACAA,EAAG,CAAC,EAAE,OAAS,EAEnB,GAAIA,GAAM,MAAQQ,GAAoBmB,GASpC,GARAI,EAAS,MAAQ;AAAA,EACjBlB,EAAML,EAAQ,IAAKR,GAAA,YAAAA,EAAK,EAAE,EAE1BoC,EAAY5B,CAAM,EAClBmB,EAAiB,KACjBC,EAAgB,GAChBH,EAAajB,EAETlB,EAAkB,KAAKS,EAAG,CAAC,CAAC,EAC9B,aAEG,CACL,MAAMwC,EAAOxC,EAAG,CAAC,EAAE,QAAQ,KAAK,OAAO4B,EAAiB,CAAC,EAAG,EAAE,EAC9Dd,EAAML,EAAQ,IAAK+B,CAAI,EAEnBV,EAAQ,KAAO,MACjBO,EAAY5B,CAAgB,EAG1BoB,EACFA,EAAgB,GAEhBG,EAAS,MAAQ;AAAA,EAGfL,EACFK,EAAS,MAAQ5B,EAAOoC,CAAI,EAE5BR,EAAS,MAAQQ,EAGnB,QACF,CACF,CAEA,GAAIxC,EAAG,CAAC,EAAE,KAAK,IAAM,GAIrB,QAAQe,GAAAR,EAAAP,EAAG,CAAC,IAAJ,KAAAO,EAASP,EAAG,CAAC,IAAb,KAAAe,EAAkBf,EAAG,CAAC,EAAG,CAE/B,IAAK,IAIH,GAHAR,EAAI,UAAY,EAChBS,EAAKT,EAAI,KAAKQ,EAAG,CAAC,CAAC,EAEfC,GAAM,KAAM,CACd,MAAMQ,IAAUQ,GAAAD,EAAAf,EAAG,CAAC,IAAJ,YAAAe,EAAO,SAAP,KAAAC,EAAiB,GAAK,GAElCa,EAAQ,KAAO,MAAQJ,GAAc,QACvCW,EAAY5B,CAAM,EAClBiB,EAAa,MAGfZ,EAAML,EAAQ,KAAMR,EAAG,CAAC,EAAGA,EAAG,CAAC,EAAGA,EAAG,CAAC,CAAC,EAEvC+B,EAAS,GAAK/B,EAAG,CAAC,EAEdQ,IAAW,IACTR,EAAG,CAAC,GAAK,IACX+B,EAAS,KAAO,QACP/B,EAAG,CAAC,IAAM,IACnB+B,EAAS,KAAO,OACP/B,EAAG,CAAC,GAAK,KAClB+B,EAAS,KAAO,QAEhBA,EAAS,KAAO,QAChBF,EAAQ,GAAKE,EAAS,KAI1B,KACF,CAEF,IAAK,IACL,IAAK,IAEL,IAAK,KAAM,CAST,GARA5C,EAAS,UAAY,EAErBa,IAAKiB,EAAAlB,EAAG,CAAC,IAAJ,KAAAkB,EAASlB,EAAG,CAAC,GAAK,MAClB,KACAZ,EAAS,KAAKY,EAAG,CAAC,CAAC,EAIpBC,GAAM,KAAM,CACd,MAAMQ,IAAUW,GAAAD,EAAAlB,EAAG,CAAC,IAAJ,YAAAkB,EAAO,SAAP,KAAAC,EAAiB,GAAK,EAChCqB,EAAKxC,EAAG,CAAC,EACTyC,EAAKzC,EAAG,CAAC,EACT0C,EAAK1C,EAAG,CAAC,IAAM,KAAOA,EAAG,CAAC,IAAM,KAChC2C,EAAKD,EAAK,KAAO1C,EAAG,CAAC,EAyB3B,GAvBAa,EAAML,EAAQ,IAAKgC,EAAIE,EAAIC,CAAE,GAG3Bd,EAAQ,KAAO,MACfA,EAAQ,OAASrB,IAEjB4B,EAAY5B,CAAM,EAGpBqB,EAAQ,OAASrB,EACjBqB,EAAQ,IAAMW,EAEdf,EAAa,KAETjB,IAAW,GAAKuB,EAAS,IAAM,OAC7BC,GAAQ,KACVR,EAAW,IAEXO,EAAS,KAAO,OAChBC,EAAOD,IAIPU,GAAM,KAER,IADA5B,EAAML,EAAQ,IAAKiC,CAAE,EACbzC,EAAKN,EAAS,KAAK+C,CAAE,GACvBzC,EAAG,CAAC,IAAM,IACZ6B,EAAQ,GAAK7B,EAAG,CAAC,EACRA,EAAG,CAAC,IAAM,IACf6B,EAAQ,OAAS,KACnBA,EAAQ,MAAQ7B,EAAG,CAAC,EAEpB6B,EAAQ,OAAS,IAAM7B,EAAG,CAAC,EAGzBA,EAAG,CAAC,IAAM,KACR6B,EAAQ,IAAM,OAChBA,EAAQ,GAAK7B,EAAG,CAAC,GAEVA,EAAG,CAAC,IAAM,QACf6B,EAAQ,OAAS,KACnBA,EAAQ,MAAQ7B,EAAG,CAAC,EAEpB6B,EAAQ,OAAS,IAAM7B,EAAG,CAAC,EAG7B6B,EAAQ,MAAM7B,EAAG,CAAC,CAAC,EAAIA,EAAG,CAAC,EAM/B,CAAC0C,GAAMC,GAAM,KACfd,EAAQ,KAAOc,EACND,IACTd,EAAgB,GAChBD,EAAiBnB,EACjBkB,EAAoB1B,EAAG,CAAC,IAAM,KAGhC,KACF,CAOA,GALAX,EAAW,UAAY,EACvBW,EAAKD,EAAG,CAAC,GAAK,KACT,KACAV,EAAW,KAAKU,EAAG,CAAC,CAAC,EAEtBC,GAAM,MAAQ6B,EAAQ,KAAO,KAAM,CACrChB,EAAM,IAAKb,EAAG,CAAC,EAAGA,EAAG,CAAC,CAAC,EAEnBA,EAAG,CAAC,IAAM,KACR6B,EAAQ,IAAM,OAChBA,EAAQ,GAAK7B,EAAG,CAAC,EAAE,KAAK,GAEjBA,EAAG,CAAC,IAAM,QACf6B,EAAQ,OAAS,KACnBA,EAAQ,OAAS,IAAM7B,EAAG,CAAC,EAAE,KAAK,EAElC6B,EAAQ,MAAQ7B,EAAG,CAAC,EAAE,KAAK,EAEpB6B,EAAQ,MAAM7B,EAAG,CAAC,CAAC,GAAK,KACjC6B,EAAQ,MAAM7B,EAAG,CAAC,CAAC,GAAKA,EAAG,CAAC,EAE5B6B,EAAQ,MAAM7B,EAAG,CAAC,CAAC,EAAIA,EAAG,CAAC,EAG7B,KACF,CAOA,GALAZ,EAAW,UAAY,EACvBY,EAAKD,EAAG,CAAC,GAAK,KACR,KACAX,EAAW,KAAKW,EAAG,CAAC,CAAC,EAEvBC,GAAM,KAAM,CACd,MAAMQ,IAAUa,GAAAD,EAAApB,EAAG,CAAC,IAAJ,YAAAoB,EAAO,SAAP,KAAAC,EAAiB,GAAK,EAQtC,QANIQ,EAAQ,KAAO,MAAQJ,GAAc,OACvCW,EAAY5B,CAAM,EAGpBK,EAAML,EAAQ,IAAKR,EAAG,CAAC,EAAGA,EAAG,CAAC,CAAC,EAEvBA,EAAG,CAAC,EAAG,CACb,IAAK,UAAW,CACd+B,EAAS,MAAQ,cAAaT,EAAAtB,EAAG,CAAC,IAAJ,KAAAsB,EAAS,MAAM,IAC7C,KACF,CACA,IAAK,MAAO,CACVS,EAAS,MAAQ,UAASR,EAAAvB,EAAG,CAAC,IAAJ,KAAAuB,EAAS,gCAAgC,KACnE,KACF,CACA,IAAK,WAAY,CACf,IAAIqB,EAAW,GAIf,IAHAb,EAAS,SAAWvB,IAAW,EAE/BX,EAAgB,UAAYX,EAAY,UAChCc,EAAKH,EAAgB,KAAKe,CAAG,GAAI,CACvC,GAAIZ,EAAG,CAAC,GAAK,MAAQ,CAAC4C,EACpBrD,EAAI,UAAY,EAChBU,EAAKV,EAAI,KAAKS,EAAG,CAAC,CAAC,EAEnB+B,EAAS,GAAK9B,EAAG,CAAC,EAClB8B,EAAS,MAAQ/B,EAAG,CAAC,UACZA,EAAG,CAAC,GAAK,MAAQ4C,EAAU,CACpC1D,EAAY,UAAYW,EAAgB,UAAY,EACpDuC,EAAY,CAAC,EACb,KACF,MACEL,EAAS,MAAQ;AAAA,EAAO/B,EAAG,CAAC,EAE9B4C,EAAW,EACb,CACF,CACF,CAEA,KACF,CAEF,CACA,QAAS,CAGP,GAFA5C,EAAKP,EAAM,KAAKM,EAAG,CAAC,CAAC,EAEjBC,GAAM,KACR,MAEF,MAAMQ,EAASR,EAAG,CAAC,EAAE,OAAS,EACxB2C,EAAK3C,EAAG,CAAC,EAAE,KAAK,EAgBtB,IAdAa,EAAML,EAAQ,IAAKR,EAAG,CAAC,CAAC,EAEpB6B,EAAQ,KAAO,MACjBO,EAAY5B,CAAM,EAElBuB,EAAS,MAAQY,GACRZ,EAAS,OAAS,QAAUA,EAAS,OAAS,GACvDA,EAAS,MAAQY,EAEjBZ,EAAS,MAAQ,IAAMY,EAGzBlB,EAAajB,EAELR,EAAKL,EAAM,KAAKgD,CAAE,GAAI,CAC5B,MAAME,EAAQd,EAAS,KAAK,OAAS/B,EAAG,MAAQ2C,EAAG,OAEnDZ,EAAS,KAAK,KAAK,CACjB,GAAI/B,EAAG,CAAC,EACR,MAAA6C,EACA,IAAKA,EAAQ7C,EAAG,CAAC,EAAE,MACrB,CAAC,CACH,CAEA,KACF,CACF,EAGFoC,EAAY,CAAC,EAEb,MAAMU,EAAM,MAAM,KAAKZ,EAAO,OAAO,CAAC,EAEtC,SAASa,EAAQhB,EAA4C,CAG3D,QAASiB,EAAIjB,EAAS,KAAK,OAAS,EAAGiB,GAAK,EAAGA,IAAK,CAClD,MAAMC,EAAMlB,EAAS,KAAKiB,CAAC,EAE3B,GAAIf,EAAQ,IAAIgB,EAAI,EAAE,GAAK,CAACf,EAAO,IAAIe,EAAI,EAAE,EAC3ClB,EAAS,KAAOA,EAAS,KAAK,MAAM,EAAGkB,EAAI,KAAK,EAC5ClB,EAAS,KAAK,MAAMkB,EAAI,GAAG,MAC1B,CACL,MAAMC,EAAQH,EAAQb,EAAO,IAAIe,EAAI,EAAE,CAAC,EAExClB,EAAS,KAAOA,EAAS,KAAK,MAAM,EAAGkB,EAAI,KAAK,EAC5CC,EAAM,KACNnB,EAAS,KAAK,MAAMkB,EAAI,GAAG,EAE3BC,EAAM,OAAS,SACjBjB,EAAQ,IAAIiB,EAAM,EAAE,CAExB,CACF,CAEA,OAAAnB,EAAS,KAAO,CAAC,EAEVA,CACT,CAEA,QAASoB,EAAI,EAAGA,EAAIjB,EAAO,KAAO,EAAGiB,IAAK,CACxC,IAAIpB,EAEAoB,IAAM,GAAKnB,GAAQ,OAEZmB,IAAM,EACfpB,EAAWC,EAEXD,EAAWe,EAAIK,EAAI,CAAC,EAGlBpB,EAAS,KAAK,SAAW,GAI7BgB,EAAQhB,CAAQ,EAClB,EAEIC,GAAA,YAAAA,EAAM,OAAQ,OAChBG,EAAO,KAAOH,EAAK,KACnBG,EAAO,SAAW,kBAGpB,QAASgB,EAAI,EAAGA,EAAIL,EAAI,OAAQK,IAAK,CACnC,IAAIC,EACJ,MAAMrB,EAAWe,EAAIK,CAAC,EAElBpB,GAAY,MAAQE,EAAQ,IAAIF,EAAS,EAAE,IAI3CA,EAAS,OAAS,QACpBqB,EAAW,OAAOrB,EAAS,EAAE,IACpBA,EAAS,OAAS,OAC3BqB,EAAW,YAAYrB,EAAS,EAAE,IACzBA,EAAS,OAAS,UAC3BqB,EAAW,YAAYrB,EAAS,EAAE,KAGpCI,EAAO,UAAUJ,EAAS,EAAE,EAAI,CAC9B,GAAIA,EAAS,GACb,SAAAqB,EACA,KAAMrB,EAAS,KACf,KAAMA,EAAS,IACjB,EACF,CAEA,OAAOI,CACT,CAGA,MAAMkB,GAAa,iDAUZ,gBAAS,gBACdC,EACAC,EACAC,EACoB,CAxjBtB,IAAAlD,EAAAQ,EAyjBE,MAAM2C,EAAKH,EAAS,QAAQD,GAAY,CAACK,EAAQC,EAAOV,IAAQ,CAC9D,GAAIA,GAAO,KAAM,CACf,MAAMlB,EAAWyB,EAAYP,CAAG,EAEhC,OAAIlB,GAAY,KAAa,GAEtBA,CACT,CAEA,OAAOwB,EAAKI,CAAK,GAAK,KAAOxD,EAAOoD,EAAKI,CAAK,EAAE,SAAS,CAAC,EAAI,EAChE,CAAC,EAED,OAAO7C,GAAAR,EAAA,OAAO,OAAO,SAASmD,CAAE,EAAE,SAAS,EAAE,CAAC,IAAvC,YAAAnD,EAA0C,OAA1C,KAAAQ,EAAkD,IAC3D",
6
+ "names": ["sniffTestRe", "element1", "directive1", "attribute1", "preformattedClose", "id1", "idnt1", "text1", "paramsRe", "refRe", "escapeRe", "templateLinesRe", "voids", "m1", "m2", "m3", "entities", "escape", "value", "match", "_a", "makeElement", "indent", "makeChunk", "type", "makeFragment", "doc", "debug", "_b", "_c", "_d", "_e", "_f", "_g", "_h", "_i", "_j", "_k", "skipping", "textIndent", "verbatimSerialize", "verbatimIndent", "verbatimFirst", "element", "chunk", "fragment", "root", "claimed", "parsed", "output", "applyIndent", "targetIndent", "attr", "line", "tg", "ar", "pr", "tx", "indented", "start", "arr", "flatten", "j", "ref", "child", "i", "selector", "templateRe", "template", "args", "getFragment", "lf", "_match", "param"]
7
+ }
@@ -0,0 +1,287 @@
1
+ import { longform, processTemplate } from './longform.ts';
2
+ import type { ParsedResult } from './types.ts';
3
+ import test from 'node:test';
4
+ import assert from 'node:assert/strict';
5
+ import vnu from 'vnu-jar';
6
+ import { execFile } from "node:child_process";
7
+ import { tmpdir } from "node:os";
8
+ import { parse, resolve } from "node:path";
9
+ import { randomUUID } from "node:crypto";
10
+ import { writeFile, unlink } from "node:fs/promises";
11
+ import * as prettier from 'prettier';
12
+
13
+ async function validate(html: string, type: 'html' | 'xml' = 'html'): Promise<boolean> {
14
+ return true;
15
+
16
+ const tmpfile = resolve(tmpdir(), randomUUID() + '.html');
17
+ const format = type === 'xml'
18
+ ? '--xml'
19
+ : '--html';
20
+
21
+ await writeFile(tmpfile, html, 'utf-8');
22
+
23
+ return new Promise<boolean>((resolve, reject) => {
24
+ execFile('java', [
25
+ '-jar',
26
+ `"${vnu}"`,
27
+ format,
28
+ '--text',
29
+ 'json',
30
+ tmpfile,
31
+ ], { shell: true }, async (err) => {
32
+ await unlink(tmpfile);
33
+
34
+ if (err) {
35
+ console.log(`HTML Validation error: ${err}`);
36
+ console.log(await prettier.format(html, { parser: 'html' }));
37
+ return reject(false);
38
+ }
39
+
40
+ resolve(true);
41
+ });
42
+ });
43
+ }
44
+
45
+ function wrapHead(html: string): string {
46
+ return `<!doctype html><html lang=en><head>${html}</head><body><h1>Test</h1></body></html>`;
47
+ }
48
+
49
+ function wrapBody(html: string): string {
50
+ return `<!doctype html><html lang=en><head><title>Test</title></head><body>${html}</body></html>`;
51
+ }
52
+
53
+ const lf1 = `
54
+ #ignored-in-test
55
+ p:: Ignore me.
56
+
57
+ @doctype:: html
58
+ html[lang=en]::
59
+ head::
60
+ title:: Longform title
61
+ body::
62
+ h1:: Longform h1
63
+ `;
64
+
65
+ const html1 = `\
66
+ <!doctype html>\
67
+ <html lang="en"><head><title>Longform title</title></head>\
68
+ <body><h1>Longform h1</h1></body></html>`;
69
+
70
+ test('It creates a root element with doctype', async () => {
71
+ const res = longform(lf1);
72
+ const html = res.root as string;
73
+
74
+ assert(await validate(html));
75
+ assert.equal(html, html1);
76
+ });
77
+
78
+ const lf2 = `\
79
+ #page-info
80
+ div.card.card--info::
81
+ h4.card-header::
82
+ The card's title goes here
83
+ p.card-description::
84
+ This is the body of the card. You
85
+ can use&nbsp;<b>html</b> to inline
86
+ elements which are hard to use in longform
87
+ syntax, <strong>but they have to be allowed
88
+ using longform directives</strong>.
89
+ `;
90
+
91
+ const html2 = `\
92
+ <div id="page-info" class="card card--info"><h4 class="card-header">The card's \
93
+ title goes here</h4><p class="card-description">\
94
+ This is the body of the card. You can use&nbsp;<b>html</b> \
95
+ to inline elements which are hard to use in \
96
+ longform syntax, <strong>but they have to be \
97
+ allowed using longform directives</strong>.</p>\
98
+ </div>`;
99
+ test('It creates an ided element with inline html copy', async () => {
100
+ const res = longform(lf2);
101
+ const html = res.fragments['page-info'].html;
102
+
103
+ assert(await validate(wrapBody(html)));
104
+ assert.equal(html, html2);
105
+ });
106
+
107
+ const lf3 = `
108
+ #head [
109
+ title:: My Longform Test
110
+ meta::
111
+ [name=description]
112
+ [content=This tests the validity and correctness of the longform output]
113
+ ]
114
+ `;
115
+ const html3 = `\
116
+ <title>My Longform Test</title>\
117
+ <meta name="description" content="This tests the validity and correctness of the longform output">\
118
+ `;
119
+ test('It creates a range of elements', async () => {
120
+ const res = longform(lf3);
121
+ const html = res.fragments['head'].html as string;
122
+
123
+ assert.equal(html, html3);
124
+ });
125
+
126
+ const lf4 = `
127
+ @xml:: version="1.0" encoding="UTF-8"
128
+ h:html::
129
+ [xmlns:xdc=http://www.xml.com/books]
130
+ [xmlns:h=http://www.w3.org/HTML/1998/html4]
131
+ h:head::
132
+ h:title:: Book Review
133
+ h:body::
134
+ xdc:bookreview::
135
+ xdc:title:: XML: A Primer
136
+ h:table::
137
+ h:tr[align=center]::
138
+ h:td:: Author
139
+ h:td:: Price
140
+ h:td:: Pages
141
+ h:td:: Date
142
+ h:tr[align=left]::
143
+ h:td::
144
+ xdc:author:: Simon St. Laurent
145
+ h:td::
146
+ xdc:price:: 31.98
147
+ h:td::
148
+ xdc:pages:: 352
149
+ h:td::
150
+ xdc:date:: 1998/01
151
+ `;
152
+
153
+ const xml4 = `\
154
+ <?xml version="1.0" encoding="UTF-8"?>\
155
+ <h:html xmlns:xdc="http://www.xml.com/books" xmlns:h="http://www.w3.org/HTML/1998/html4">\
156
+ <h:head><h:title>Book Review</h:title></h:head>\
157
+ <h:body>\
158
+ <xdc:bookreview>\
159
+ <xdc:title>XML: A Primer</xdc:title>\
160
+ <h:table>\
161
+ <h:tr align="center">\
162
+ <h:td>Author</h:td><h:td>Price</h:td>\
163
+ <h:td>Pages</h:td><h:td>Date</h:td></h:tr>\
164
+ <h:tr align="left">\
165
+ <h:td><xdc:author>Simon St. Laurent</xdc:author></h:td>\
166
+ <h:td><xdc:price>31.98</xdc:price></h:td>\
167
+ <h:td><xdc:pages>352</xdc:pages></h:td>\
168
+ <h:td><xdc:date>1998/01</xdc:date></h:td>\
169
+ </h:tr>\
170
+ </h:table>\
171
+ </xdc:bookreview>\
172
+ </h:body>\
173
+ </h:html>\
174
+ `;
175
+ test('It parses an XML string', () => {
176
+ const res = longform(lf4);
177
+ const xml = res.root as string;
178
+
179
+ assert.equal(xml, xml4);
180
+ })
181
+
182
+ const lf5 = `
183
+ pre::
184
+ code:: {
185
+ div::
186
+ Example longform
187
+ <em>with preformatted html</em>
188
+ }
189
+ `;
190
+ const html5 = `<pre><code>div::
191
+ Example longform
192
+ &lt;em&gt;with preformatted html&lt;/em&gt;
193
+ </code></pre>`;
194
+ test('It parses preformatted content', () => {
195
+ const res = longform(lf5);
196
+ const html = res.root as string;
197
+
198
+ assert.equal(html, html5);
199
+ })
200
+
201
+ const lf6 = `
202
+ head::
203
+ script:: {{
204
+ const foo = 'bar';
205
+
206
+ console.log(foo);
207
+ }}
208
+ `;
209
+ const html6 = `\
210
+ <head><script>const foo = 'bar';
211
+ console.log(foo);
212
+ </script></head>\
213
+ `;
214
+ test('It parses preformatted content', () => {
215
+ const res = longform(lf6);
216
+ const html = res.root as string;
217
+
218
+ assert.equal(html, html6);
219
+ })
220
+
221
+ const lf7 = `
222
+ #meta [
223
+ title:: Ref test
224
+ meta::
225
+ [name=description]
226
+ [content=Tests referencing other frags]
227
+ ]
228
+
229
+ #footer::
230
+ footer::
231
+ This is the footer
232
+
233
+ @doctype:: html
234
+ html[lang=en]::
235
+ head::
236
+ #[meta]
237
+ body::
238
+ Test #[header]
239
+ #[main]
240
+ #[footer]
241
+
242
+ #header
243
+ header::
244
+ hgroup::
245
+ h1:: Ref test
246
+ p:: Tests referenceing other frags
247
+
248
+ #p2
249
+ p::
250
+ The second p tag.
251
+
252
+ #main
253
+ main::
254
+ #[p1]
255
+ #[p2]
256
+
257
+ #p1
258
+ p::
259
+ The first p tag.
260
+ `;
261
+
262
+ test('It embeds referenced fragments', { skip: true }, async () => {
263
+ const res = longform(lf7);
264
+ });
265
+
266
+ const lf8 = `
267
+ @root
268
+ html::
269
+ This is ignored
270
+
271
+ @template
272
+ #label "
273
+ This is my #{position} label text #[ref]
274
+ with a new line in it
275
+ "
276
+
277
+ ##ref
278
+ p::
279
+ This is referenced.
280
+ "
281
+ `;
282
+
283
+ test('It renders plan text fragments', { skip: true }, () => {
284
+ const parsed = longform(lf8);
285
+ const res = processTemplate('label', { position: 4 }, parsed);
286
+ })
287
+