------------------------------------------------------------
commit 81fd5d3254f2bbef4d3e47fe4b6bbbe7599c74ff
Author: Breck Yunits <breck7@gmail.com>
Date: Sat Dec 7 15:00:05 2024 -1000
diff --git a/.nojekyll b/.nojekyll
new file mode 100644
index 0000000..e69de29
------------------------------------------------------------
commit 6fb04c5f2c7ba908e165232759773cd54a94053e
Author: Breck Yunits <breck7@gmail.com>
Date: Sat Dec 7 13:54:48 2024 -1000
diff --git a/dist/constants.js b/dist/constants.js
index 5e7d3d2..3225916 100644
--- a/dist/constants.js
+++ b/dist/constants.js
@@ -1 +1 @@
-const AppConstants = {"parsers":"columnNameAtom\n extends stringAtom\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n extends stringAtom\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint comment\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n extends anyAtom\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\nscriptAnyAtom\n extends codeAtom\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom keywordAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint string\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\njavascriptAtom\n extends stringAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\"<a \") ? false : true\n return shouldLinkify ? this.replaceNotes(Utils.linkify(originalText)) : originalText\n }\n replaceNotes(originalText) {\n // Skip the replacements if there are no footnotes or the text has none.\n if (!this.root.footnotes.length || !originalText.includes(\"^\")) return originalText\n this.root.footnotes.forEach((note, index) => {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `<a href=\"#${note.htmlId}\" class=\"scrollNoteLink\" id=\"${linkBack}\"><sup>${note.label}</sup></a>`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.text}${this.closingTag}`\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return `</${tag}>`\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `<span class=\"scrollBlink\">${super.buildHtml()}</span>\n <script>setInterval(()=>{ Array.from(document.getElementsByClassName(\"scrollBlink\")).forEach(el => el.style.visibility = el.style.visibility === \"hidden\" ? \"visible\" : \"hidden\") }, 500)</script>`\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"linkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"</center>\")\n return `<center>${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser\n javascript\n compileSubparticles() {\n return this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildHtml() {\n return super.buildHtml() + this.compileSubparticles()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `<div style=\"text-indent:${(this.getIndentLevel() - 1) * 20}px;\"><input type=\"checkbox\" ${this.checked} id=\"${this.id}\"><label for=\"${this.id}\">` + super.text + `</label></div>`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `</${listType}>` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return `<style>\\n.${this.constructor.name} li::marker {content: \"${this.cue} \";}\\n</style>`\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* <span id=\"counter${id}\" title=\"0\">0</span><script>setInterval(()=>{ const el = document.getElementById('counter${id}'); el.title = parseFloat(el.title) + ${increment}; el.textContent = Math.floor(parseFloat(el.title)).toLocaleString()}, 100)</script> ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"</details>\")\n return `<details>${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `<a class=\"scrollFootNoteUsageLink\" href=\"#noteUsage${this.noteDefinitionIndex}\">${this.label}</a> ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"</div>\")\n return `<div class=\"scrollSection\">` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}></${this.tag}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return `<audio controls ${this.getAsHtmlAttributes(\"loop autoplay\".split(\" \"))}><source src=\"${this.filename}\" type=\"audio/mpeg\"></audio>`\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom\n heightParser\n cueFromId\n atoms cueAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\n widthParser\n // todo: fix inheritance bug\n cueFromId\n atoms cueAtom\n heightParser\n cueFromId\n atoms cueAtom\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* <span class=\"scrollStopwatchParser\" id=\"stopwatch${id}\">0.0</span><script>{let startTime = parseFloat(new URLSearchParams(window.location.search).get('start') || 0); document.getElementById('stopwatch${id}').title = startTime; setInterval(()=>{ const el = document.getElementById('stopwatch${id}'); el.title = parseFloat(el.title) + .1; el.textContent = (parseFloat(el.title)).toFixed(1)}, 100)}</script> `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"</div>\") // Single columns are self-closing after section break.\n return stackContents + `<div class=\"scrollColumns\" style=\"column-width:${columnWidth}ch;column-count:${maxColumns};max-width:${maxTotalWidth}ch;\">`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"</div>\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"</div>\")\n return `<style>.scrollContainerParser{width: 100%; box-sizing: border-box; max-width: ${this.maxWidth}; margin: 0 auto;}</style>` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `<div class=\"${this.defaultClass}\"><span>${this.dinkus}</span></div>`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n javascript\n buildHtml() {\n return `<hr>`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `<style>.abstractIconButtonParser {position:absolute;top:0.25rem; }.abstractIconButtonParser svg {fill: rgba(204,204,204,.8);width:1.875rem;height:1.875rem; padding: 0 7px;} .abstractIconButtonParser:hover svg{fill: #333;}</style><a href=\"${this.link}\" class=\"doNotPrint abstractIconButtonParser\" style=\"${this.style}\">${this.svg}</a>`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg <svg fill=\"#000000\" xmlns=\"http://www.w3.org/2000/svg\" width=\"800px\" height=\"800px\" viewBox=\"0 0 52 52\" enable-background=\"new 0 0 52 52\" xml:space=\"preserve\"><path d=\"M38.6,20.4c-1-6.5-6.7-11.5-13.5-11.5c-7.6,0-13.7,6.1-13.7,13.7c0,0.3,0,0.7,0.1,1c-5,0.4-8.9,4.6-8.9,9.6 c0,5.4,4.3,9.7,9.7,9.7h11.5c-0.8-0.8-8.1-8.1-8.1-8.1c-0.4-0.4-0.4-0.9,0-1.3l1.3-1.3c0.4-0.4,0.9-0.4,1.3,0l3.5,3.5 c0.4,0.4,1.1,0.1,1.1-0.4V21.8c0-0.4,0.5-0.9,1-0.9h1.9c0.5,0,0.9,0.4,0.9,0.9v13.4c0,0.6,0.8,0.8,1.1,0.4l3.5-3.5 c0.4-0.4,0.9-0.4,1.3,0l1.3,1.3c0.4,0.4,0.4,0.9,0,1.3L26,42.9h12.3v0c6.1-0.1,11-5.1,11-11.3C49.4,25.5,44.6,20.6,38.6,20.4z\"/></svg><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->\n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg <svg width=\"800px\" height=\"800px\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M21.1213 2.70705C19.9497 1.53548 18.0503 1.53547 16.8787 2.70705L15.1989 4.38685L7.29289 12.2928C7.16473 12.421 7.07382 12.5816 7.02986 12.7574L6.02986 16.7574C5.94466 17.0982 6.04451 17.4587 6.29289 17.707C6.54127 17.9554 6.90176 18.0553 7.24254 17.9701L11.2425 16.9701C11.4184 16.9261 11.5789 16.8352 11.7071 16.707L19.5556 8.85857L21.2929 7.12126C22.4645 5.94969 22.4645 4.05019 21.2929 2.87862L21.1213 2.70705ZM18.2929 4.12126C18.6834 3.73074 19.3166 3.73074 19.7071 4.12126L19.8787 4.29283C20.2692 4.68336 20.2692 5.31653 19.8787 5.70705L18.8622 6.72357L17.3068 5.10738L18.2929 4.12126ZM15.8923 6.52185L17.4477 8.13804L10.4888 15.097L8.37437 15.6256L8.90296 13.5112L15.8923 6.52185ZM4 7.99994C4 7.44766 4.44772 6.99994 5 6.99994H10C10.5523 6.99994 11 6.55223 11 5.99994C11 5.44766 10.5523 4.99994 10 4.99994H5C3.34315 4.99994 2 6.34309 2 7.99994V18.9999C2 20.6568 3.34315 21.9999 5 21.9999H16C17.6569 21.9999 19 20.6568 19 18.9999V13.9999C19 13.4477 18.5523 12.9999 18 12.9999C17.4477 12.9999 17 13.4477 17 13.9999V18.9999C17 19.5522 16.5523 19.9999 16 19.9999H5C4.44772 19.9999 4 19.5522 4 18.9999V7.99994Z\"/></svg>\n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg <svg viewBox=\"3 5 24 20\" width=\"24\" height=\"20\" xmlns=\"http://www.w3.org/2000/svg\"><g transform=\"matrix(1, 0, 0, 1, 0, -289.0625)\"><path style=\"opacity:1;stroke:none;stroke-width:0.49999997;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1\" d=\"M 5 5 C 4.2955948 5 3.6803238 5.3628126 3.3242188 5.9101562 L 14.292969 16.878906 C 14.696939 17.282876 15.303061 17.282876 15.707031 16.878906 L 26.675781 5.9101562 C 26.319676 5.3628126 25.704405 5 25 5 L 5 5 z M 3 8.4140625 L 3 23 C 3 24.108 3.892 25 5 25 L 25 25 C 26.108 25 27 24.108 27 23 L 27 8.4140625 L 17.121094 18.292969 C 15.958108 19.455959 14.041892 19.455959 12.878906 18.292969 L 3 8.4140625 z \" transform=\"translate(0,289.0625)\" id=\"rect4592\"/></g></svg>\n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg <svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12.7166 3.79541C12.2835 3.49716 11.7165 3.49716 11.2834 3.79541L4.14336 8.7121C3.81027 8.94146 3.60747 9.31108 3.59247 9.70797C3.54064 11.0799 3.4857 13.4824 3.63658 15.1877C3.7504 16.4742 4.05336 18.1747 4.29944 19.4256C4.41371 20.0066 4.91937 20.4284 5.52037 20.4284H8.84433C8.98594 20.4284 9.10074 20.3111 9.10074 20.1665V15.9754C9.10074 14.9627 9.90433 14.1417 10.8956 14.1417H13.4091C14.4004 14.1417 15.204 14.9627 15.204 15.9754V20.1665C15.204 20.3111 15.3188 20.4284 15.4604 20.4284H18.4796C19.0806 20.4284 19.5863 20.0066 19.7006 19.4256C19.9466 18.1747 20.2496 16.4742 20.3634 15.1877C20.5143 13.4824 20.4594 11.0799 20.4075 9.70797C20.3925 9.31108 20.1897 8.94146 19.8566 8.7121L12.7166 3.79541ZM10.4235 2.49217C11.3764 1.83602 12.6236 1.83602 13.5765 2.49217L20.7165 7.40886C21.4457 7.91098 21.9104 8.73651 21.9448 9.64736C21.9966 11.0178 22.0564 13.5119 21.8956 15.3292C21.7738 16.7067 21.4561 18.4786 21.2089 19.7353C20.9461 21.0711 19.7924 22.0001 18.4796 22.0001H15.4604C14.4691 22.0001 13.6655 21.1791 13.6655 20.1665V15.9754C13.6655 15.8307 13.5507 15.7134 13.4091 15.7134H10.8956C10.754 15.7134 10.6392 15.8307 10.6392 15.9754V20.1665C10.6392 21.1791 9.83561 22.0001 8.84433 22.0001H5.52037C4.20761 22.0001 3.05389 21.0711 2.79113 19.7353C2.54392 18.4786 2.22624 16.7067 2.10437 15.3292C1.94358 13.5119 2.00338 11.0178 2.05515 9.64736C2.08957 8.73652 2.55427 7.91098 3.28346 7.40886L10.4235 2.49217Z\"/></svg>\n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg <svg xmlns=\"http://www.w3.org/2000/svg\" direction=\"ltr\" width=\"231.52568231268793\" height=\"249.33156169975996\" viewBox=\"297.27169239534896 1273.121785992385 231.52568231268793 249.33156169975996\" stroke-linecap=\"round\" stroke-linejoin=\"round\" data-color-mode=\"dark\" class=\"tl-container tl-theme__force-sRGB tl-theme__dark\" ><defs/><g transform=\"matrix(1, 0, 0, 1, 395.9682, 1413.3618)\" opacity=\"1\"><g transform=\"scale(1)\"><path d=\"M-5.7342,-1.1484 T-5.0182,-4.6536 -2.6672,-12.9768 1.7374,-22.3903 8.4597,-30.0892 16.5455,-35.2796 25.3478,-37.8247 34.4641,-37.2199 43.0676,-33.4606 50.27,-27.1118 55.7861,-19.7972 59.8042,-11.6764 61.6844,-2.3157 60.8049,7.9615 57.3259,17.5954 51.2605,25.5515 41.9451,31.9237 30.6489,36.2084 18.9812,37.6038 8.4915,36.637 -1.3063,34.5174 -10.6869,31.6604 -19.0463,27.0252 -26.0612,20.1562 -31.864,11.1271 -35.2772,1.0066 -35.7963,-9.1933 -35.203,-19.2337 -32.52,-28.3259 -27.3388,-37.4245 -20.2857,-45.7085 -11.787,-53.0395 -2.4314,-58.4864 7.1724,-61.8739 17.5002,-65.6025 28.2859,-68.1034 38.514,-69.1081 48.1844,-69.9134 57.9136,-70.1787 67.4056,-69.5971 76.297,-67.7446 84.5233,-63.97 92.0158,-57.5003 98.1976,-48.5614 102.8839,-38.056 105.9936,-27.6097 107.074,-18.168 107.175,-8.2882 106.6452,1.9768 105.4734,11.7698 104.0279,21.1013 101.7439,31.0445 98.0977,40.6386 92.7822,49.1039 86.2847,57.2337 78.6099,64.7113 69.8339,70.974 61.2863,75.092 52.4283,78.2186 42.5338,80.7945 32.7733,82.6629 22.8568,83.6574 12.2803,83.3538 2.4971,81.4828 -6.3367,78.789 -15.1061,75.8011 -24.259,72.3779 -33.2286,68.991 -41.5707,64.8363 -49.153,59.1909 -55.8375,52.2938 -62.9311,43.9553 -68.4604,34.4882 -71.2761,23.5855 -72.748,12.7452 -73.7673,2.0453 -74.1187,-8.7386 -72.293,-19.3856 -68.7376,-30.4053 -64.12,-39.7852 -59.1825,-47.8931 -53.7695,-56.4512 -48.1998,-65.7676 -43.1389,-74.401 -36.9006,-81.0179 -28.4317,-86.2639 -18.3342,-90.3521 -8.9259,-93.9355 0.1927,-98.7179 9.3551,-103.3537 18.7651,-106.7552 29.7482,-110.3584 40.9287,-113.446 50.87,-115.2231 57.8279,-116.0074 60.4065,-116.0632 A7.8326,7.8326 0 0 1 61.1735,-100.4168 T58.6018,-100.2201 51.1634,-99.3008 41.695,-96.884 32.4044,-94.0187 22.65,-91.382 13.4974,-87.3756 5.0519,-83.1026 -5.0443,-78.6175 -15.7236,-74.4743 -24.6193,-70.2681 -31.274,-62.5903 -36.3827,-53.9362 -41.2833,-46.2905 -46.0679,-38.5161 -50.639,-30.6576 -54.6876,-22.4063 -57.6038,-12.3346 -58.5104,-1.8678 -57.6022,9.4969 -56.0628,20.477 -53.6434,29.0487 -48.8908,36.7517 -42.4075,43.9784 -35.3064,50.6236 -27.3553,55.2062 -17.6884,59.4064 -7.6442,63.6638 2.4427,67.0449 12.7308,69.4843 22.9294,70.2171 33.2547,69.3921 42.4972,67.7384 52.2753,64.8425 61.8649,60.9142 69.5292,56.0828 76.3604,49.5927 82.431,42.3194 87.219,34.619 90.6076,25.5217 92.547,16.0352 93.8703,7.1315 94.4972,-2.5268 94.3672,-13.3681 93.3204,-24.4252 90.5377,-34.1656 85.7321,-43.6441 78.7334,-51.8057 68.7542,-56.0559 57.9857,-57.2713 48.079,-57.0198 38.4535,-56.3204 28.5246,-55.0036 18.6104,-52.4796 9.6402,-49.559 0.7193,-46.2981 -7.1531,-41.3715 -13.708,-34.8957 -19.5435,-26.853 -22.8356,-17.0622 -23.363,-6.5446 -21.9969,3.1439 -17.1086,11.9113 -9.276,18.744 -0.0685,22.6058 9.3636,24.796 19.2422,25.8817 29.052,25.0411 37.9338,21.3502 45.6819,13.8498 49.6675,4.5228 49.2802,-5.1854 45.3277,-14.4455 38.8603,-21.7879 30.5488,-25.7753 21.3356,-24.3975 13.6687,-19.2391 8.8217,-10.9919 6.423,-2.3609 5.7342,1.1484 A5.8481,5.8481 0 0 1 -5.7342,-1.1484 Z\" stroke-linecap=\"round\"/></g></g></svg>\n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return `<div class=\"abstractTextLinkParser\"><a href=\"${this.link}\">${this.text}</a></div>`\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n <script>\n sendFormViaEmail = form => {\n const mailto = new URL(\"mailto:\")\n const params = []\n const { value, title } = form.querySelector('button[type=\"submit\"]')\n params.push(`subject=${encodeURIComponent(value)}`)\n params.push(`to=${encodeURIComponent(title)}`)\n const oneTextarea = form.querySelector('textarea[title=\"oneTextarea\"]')\n const body = oneTextarea ? codeMirrorInstance.getValue() : Array.from(new FormData(form)).map(([name, value]) => `${name} ${value}`).join(\"\\\\n\")\n params.push(`body=${encodeURIComponent(body)}`)\n mailto.search = params.join(\"&\")\n window.open(mailto.href, '_blank')\n }\n </script>\n string style\n <style> .scrollFormParser {\n font-family: \"Gill Sans\", \"Bitstream Vera Sans\", sans-serif;\n }\n .scrollFormParser input , .scrollFormParser textarea{\n padding: 10px;\n margin-bottom: 10px;\n width: 100%;\n box-sizing: border-box;\n } .scrollFormParser label {\n display: block;\n margin-bottom: 5px;\n }\n </style>\n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = `<textarea placeholder=\"${placeholder}\" id=\"${Name}\" name=\"${Name}\" ${IsRequired ? \"required\" : \"\"}></textarea>`\n else\n tag = `<input placeholder=\"${placeholder}\" type=\"${type}\" id=\"${Name}\" name=\"${Name}\" ${IsRequired ? \"required\" : \"\"}>`\n return `<div><label for=\"${Name}\" title=\"${IsRequired ? \"Required\" : \"\"}\">${ucFirst}${IsRequired ? \"*\" : \"\"}:</label>${tag}</div>`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}<form ${isEmail ? \"onsubmit='sendFormViaEmail(this); return false;'\" : ` method='post' action='${formDestination}'`} class=\"scrollFormParser\">${this.inputs}<button value=\"${subject}\" title=\"${formDestination}\" class=\"scrollButton\" type=\"submit\">${callToAction}</button>${this.footer}</form>`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n <link rel=\"stylesheet\" href=\".codeMirror.css\">\n <script src=\".scrollLibs.js\"></script>\n <script src=\".constants.js\"></script>\n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `<textarea title=\"oneTextarea\" rows=\"${Math.min(this.root.measures.length * 2, 30)}\" placeholder=\"${this.placeholder}\" id=\"${Name}\" name=\"${Name}\"></textarea>\n <script id=\"${Name}Parsers\" type=\"text/plain\">${this.parsersBundle}</script>\n <script>{\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n let {width, height} = document.getElementById('${Name}').getBoundingClientRect();\n const sp = new Particle(AppConstants.parsers)\n sp.filter(particle => particle.getLine().match(parserRegex)).forEach(part => {\n part.appendLine(\"boolean suggestInAutocomplete false\")\n })\n const customScrollParser = new HandParsersProgram(sp.toString() + document.getElementById(\"${Name}Parsers\").textContent).compileAndReturnRootParser()\n codeMirrorInstance = new ParsersCodeMirrorMode(\"custom\", () => customScrollParser, undefined, CodeMirror).register().fromTextAreaWithAutocomplete(document.getElementById(\"${Name}\"), {\n lineWrapping: false,\n lineNumbers: false\n })\n codeMirrorInstance.setSize(width, height);\n codeMirrorInstance.setValue(\\`${this.value}\\`); }</script>`\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `<a class=\"scrollContinueReadingLink\" href=\"${linkRelativeToCompileTarget}\">Continue reading...</a>`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `<div class=\"scrollSnippetContainer\">${this.makeSnippet(file.file.scrollProgram, buildSettings)}</div>`\n }).join(\"\\n\\n\")\n return `<div class=\"scrollColumns\" style=\"column-width:35ch;\">${snippets}</div>`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue nav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return `<nav class=\"scrollNavParser\">` + this.root.getFilesByTags(this.content).map(file => {\n const { linkTitle, permalink } = file.file.scrollProgram\n return `<a href=\"${permalink}\">${linkTitle}</a>` \n }).join(this.get(\"join\") || \" \") + `</nav>`\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `<div><a href=\"${permalink}\">${title}</a><div>${description}...</div><div class=\"subdued\" style=\"text-align:right;\">${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}</div></div>`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `<div id=\"${id}\" class=\"scrollNoticesParser\" style=\"display:none;\"></div><script>(function(){\n const params = new URLSearchParams(window.location.search)\n if (!params.size) return\n document.getElementById('${id}').innerHTML = Array.from(params.entries()).map(([key, value]) => {\n if (key === \"error\") \n return '<div style=\"color:red\">Error: ' + value + '</div>'\n if (key === \"success\")\n return '<div style=\"color:green\">Success: ' + value + '</div>'\n return '<div>' + key + ': ' + value + '</div>'\n }).join(\"\") || \"No query parameters found\"\n document.getElementById('${id}').style.display = \"block\"\n })()</script>`\n }\nprintSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.buildTxt().replace(/\\</g, \"<\")}</code>`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual} = this\n const expected = this.subparticlesToString()\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType anyAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n javascript\n buildHtml() {\n return `<div class=\"printDateParser\">${this.day}</div>`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne() { await this._buildFileType(this.extension) }\nbuildCsvParser\n popularity 0.000096\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo() {\n await this._buildFileType(this.extension)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(externalFilesCopied) {\n await this._copyExternalFiles(externalFilesCopied)\n await super.buildTwo()\n }\n async _copyExternalFiles(externalFilesCopied = {}) {\n if (!this.isNodeJs()) return\n const {root} = this\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Write RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Write concepts to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom anyAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildCommandParser\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Write measures to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom anyAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo() {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${this.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the <html lang=\"\"> tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Meta tag description.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType anyAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType anyAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `<div style=\"text-align: ${index % 2 ? \"right\" : \"left\"};\" class=\"scrollChat ${index % 2 ? \"scrollChatRight\" : \"scrollChatLeft\"}\"><span>${line.asString}</span></div>` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = typeof d3 === \"undefined\" ? require('d3') : d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = typeof d3 === \"undefined\" ? require('d3') : d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/</g, \"<\")\n return {\n title, titleLink: relativePath + permalink, text, date, wordCount, minutes\n }\n }\n columnNames = \"title titleLink text date wordCount minutes\".split(\" \")\nscrollPostsParser\n popularity 0.000024\n cue posts\n description Posts as table.\n extends abstractPostsParser\n example\n // Print a search table:\n posts\n printTable\n tableSearch\n // Dump a CSV of blog:\n buildHtml\n buildCsv\n buildJson\nscrollPostsMetaParser\n popularity 0.000024\n cue postsMeta\n description Post meta as table.\n extends abstractPostsParser\n javascript\n columnNames = [\"date\", \"year\", \"title\", \"permalink\", \"authors\", \"tags\", \"wordCount\", \"minutes\"]\n postToRow(file) {\n const {date, year, title, permalink, authors, tags, wordCount, minutes} = file.file.scrollProgram\n return {\n date, year, title, permalink, authors, tags, wordCount, minutes\n }\n }\nprintFeedParser\n popularity 0.000048\n description Print group to RSS.\n extends abstractPostsParser\n example\n printFeed index\n printFeed cars/index\n buildRss\n javascript\n buildRss() {\n const {dayjs} = this.root\n const scrollPrograms = this.files.map(file => file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>\n <rss version=\"2.0\">\n <channel>\n <title>${title}</title>\n <link>${baseUrl}</link>\n <description>${description}</description>\n <lastBuildDate>${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}</lastBuildDate>\n <language>en-us</language>\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n </channel>\n </rss>`\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.code.replace(/\\</g, \"<\")}</code>`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `<div class=\"codeWithHeader\"><div class=\"codeHeader\">${this.content}</div>${super.buildHtml()}</div>`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n <script>\n document.addEventListener(\"DOMContentLoaded\", () => document.querySelectorAll(\".scrollCodeBlock\").forEach(block =>\n {\n if (!navigator.clipboard) return\n const button = document.createElement(\"span\")\n button.classList.add(\"scrollCopyButton\")\n block.appendChild(button)\n button.addEventListener(\"click\", async () => {\n await navigator.clipboard.writeText(block.innerText)\n button.classList.add(\"scrollCopiedButton\")\n })\n }\n ))\n </script>\nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = `<style>\n .heatrixContainer {\n margin: auto;\n }.heatrixRow {white-space: nowrap;}\n ${cssId} .heatrixAtom {\n font-family: arial;\n border-radius: 2px;\n border: 1px solid transparent;\n display: inline-block;\n margin: 1px;\n text-align: center;\n vertical-align: middle;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n .heatrixAtom a {\n color: black;\n }\n ${cssId} .heatrixAtom{\n width: ${defaultWidth};\n height: ${defaultHeight};\n font-size: ${fontSize};\n line-height: ${lineHeight};\n }\n </style>`\n const firstRow = this.table[0]\n return (\n `<div class=\"heatrixContainer\" id=\"heatrix${this.uid}\">${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `<div class=\"heatrixRow heatrixRow${rowIndex}\">${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return `<div class=\"heatrixAtom heatrixColumn${columnIndex}${valueClass}\" style=\"${inlineStyle}\"><a title=\"${label}\" ${href}>${label}</a></div>`\n })\n .join(\"\")}</div>`\n })\n .join(\"\\n\") +\n \"</div>\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom anyAtom\n cueFromId\n single\n colorParser\n atoms cueAtom anyAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType anyAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n <link rel=\"stylesheet\" href=\".leaflet.css\">\n <script src=\".leaflet.js\"></script>\n <script src=\".scrollLibs.js\"></script>\n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `<div id=\"${mapId}\" style=\"${style}\"></div>\n <script>\n {\n if (!window.maps) window.maps = {}\n const moveToMyLocation = () => {\n if (!navigator.geolocation) return\n navigator.geolocation.getCurrentPosition((position) => {\n const { latitude, longitude } = position.coords\n window.maps.${mapId}.setView([latitude, longitude])\n L.circleMarker([latitude, longitude], {\n fillOpacity: 0.8,\n radius: 20,\n weight: 4\n })\n .addTo(window.maps.${mapId})\n }, () => {})\n }\n const lat = ${this.get(\"lat\") ?? 37.8}\n const long = ${this.get(\"long\") ?? 4}\n if (${this.has(\"geolocate\")}){\n moveToMyLocation()\n }\n const zoomLevel = ${this.get(\"zoom\") ?? 4}\n const hover = '${this.get(\"hover\") || \"<b>{title}</b><br>{description}\"}'\n const template = ${JSON.stringify(template)}\n const points = ${JSON.stringify((this.parent.coreTable || []).filter(point => point.lat && point.long), undefined, 2)}\n window.maps.${mapId} = L.map(\"map${id}\").setView([lat, long], zoomLevel)\n const map = window.maps.${mapId}\n const tileOptions = {\n \"default\": {\n baseLayer: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',\n attribution: '<a href=\"https://www.openstreetmap.org/\">OpenStreetMap</a> contributors'\n },\n light: {\n baseLayer: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',\n attribution: '<a href=\"https://www.openstreetmap.org/\">OpenStreetMap</a> contributors <a href=\"https://carto.com/ attributions\">CARTO</a>'\n },\n }\n const {baseLayer, attribution} = tileOptions.${this.get(\"tiles\") || \"default\"}\n L.tileLayer(baseLayer, {\n attribution,\n maxZoom: 19\n }).addTo(map);\n points.forEach(point => {\n L.circleMarker([point.lat, point.long], {...template, ...Object.fromEntries(\n Object.entries(point).filter(([key, value]) => value !== null)\n )})\n .addTo(map)\n .bindPopup(new Particle(point).evalTemplateString(hover))\n })\n }\n </script>`\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n <script src=\".d3.js\"></script>\n <script src=\".plot.js\"></script>\n example\n plot\n inScope abstractColumnNameParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `<div id=\"${id}\"></div><script>\n {\n let loadChart = async () => {\n const data = ${this.dataCode}\n const get = (col, index ) => col !== \"undefined\" ? col : (index === undefined ? undefined : Object.keys(data[0])[index])\n document.querySelector(\"#${id}\").append(Plot.plot(${this.plotOptions}))\n }\n loadChart()\n }\n </script>`\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n }`\n }\nscatterplotParser\n extends abstractPlotParser\n description Scatterplot Widget.\n // todo: make copyFromExternal work with inheritance\n string copyFromExternal .d3.js .plot.js\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true,\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce <script src=\".sparkline.js\"></script>\n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return `<span id=\"${id}\"></span><script>new Sparkline(document.getElementById(\"${id}\"), {dotRadius: 0, width: ${width}, height: ${height}, lineColor: \"${lineColor}\", tooltip: (value,index) => ${start} + index + \": \" + value}).draw(${JSON.stringify(columnValues)})</script>`\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `<th>${column.name}</th>\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `<a href=\"${link}\">${text}</a>`\n else if (col.name.endsWith(\"Url\")) tagged = `<a href=\"${content}\">${col.name.replace(\"Url\", \"\")}</a>`\n else if (isUrl) tagged = `<a href=\"${content}\">${text}</a>`\n str += `<td>${tagged}</td>\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `<tr>${this.toRow(row)}</tr>`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `<table id=\"table${this._getUid()}\" class=\"scrollTable\">\n <thead><tr>${this.tableHeader.join(\"\\n\")}</tr></thead>\n <tbody>${this.tableBody}</tbody>\n </table>`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n <link rel=\"stylesheet\" href=\".katex.min.css\">\n <script defer src=\".katex.min.js\"></script>\n <script>\n document.addEventListener(\"DOMContentLoaded\", () => document.querySelectorAll(\".scrollKatex\").forEach(el =>\n {\n katex.render(el.innerText, el, {\n throwOnError: false\n });\n }\n ))\n </script>\n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `<div class=\"scrollKatex\" id=\"${id}\">${content + this.subparticlesToString()}</div>`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `<style>#helpfulNotFound{margin: 100px 0;}</style><h1 id=\"helpfulNotFound\"></h1><script defer src=\"/.helpfulNotFound.js\"></script><script>document.addEventListener(\"DOMContentLoaded\", () => new NotFoundApp('${this.content}'))</script>`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `<style>html {font-size: var(--scrollBaseFontSize, 28px);} body {margin: auto; width: 500px;}.slideshowNav{text-align: center; margin-bottom:20px; font-size: 24px;color: rgba(204,204,204,.8);} a{text-decoration: none; color: rgba(204,204,204,.8);}</style><script defer src=\".jquery-3.7.1.min.js\"></script><div class=\"slideshowNav\"></div><script defer src=\".slideshow.js\"></script>`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n <script defer src=\".jquery-3.7.1.min.js\"></script>\n <style>.dt-search{font-family: \"SF Pro\", \"Helvetica Neue\", \"Segoe UI\", \"Arial\";}</style>\n <link rel=\"stylesheet\" href=\".datatables.css\">\n <script defer src=\".datatables.js\"></script>\n <script defer src=\".dayjs.min.js\"></script>\n <script defer src=\".tableSearch.js\"></script>\n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>${this.css}</style>`\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCs() {\n return this.css\n }\ninlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractScrollParser\n cueFromId\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return `<style>/* ${this.content} */\\n${this.css}</style>`\n }\n get css() {\n return this.atoms.slice(1).map(filename => this.root.readFile(filename)).join(\"\\n\\n\")\n }\n buildCs() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>html, body { background: ${this.content};}</style>`\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>html, body { color: ${this.content};}</style>`\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return `<style>html, body, h1,h2,h3 { font-family: ${font};}</style>`\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return `<link rel=\"stylesheet\" type=\"text/css\" href=\"${this.filename}\">`\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return `<script src=\"${this.filename}\"></script>`\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `<tr>` + items.map(particle => `<td>${particle.cue}<span>${particle.content}</span></td>`).join(\"\\n\") + `</tr>\\n`\n }\n buildHtml() {\n return `<table class=\"scrollDashboard\">${this.tableBody}</table>`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = this[method]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = next[method]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.code.replace(/\\</g, \"<\")}</code>`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType anyAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\ninspectBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .inspector.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `<div class=\"inspectorParticle\"><span class=\"inspectorParticleId\">${particle.constructor.name}</span>${particle.atoms.map((atom, index) => `<span class=\"inspectorAtom\">${atom}<span class=\"inspectorAtomType\">${atomTypes[index]}</span></span>`).join(\" \")}${(particle.length ? `<br><div class=\"inspectorSubparticles\">` + particle.map(mapFn).join(\"<br>\") + `</div>` : \"\")}</div>`}\n return this.selectedParticles.map(mapFn).join(\"<br>\")\n }\n buildHtml() {\n return `<link rel=\"stylesheet\" href=\".inspector.css\">` + this.code\n }\ninspectAboveParser\n description Inspect particle above.\n extends inspectBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return `<style>${this.css}</style>`\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCs() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(`</${tag}>`)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `<br>`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => `<iframe src=\"${url}\" frameborder=\"0\"></iframe>`).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `<figcaption>${caption.buildHtml()}</figcaption>` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `<figure class=\"scrollCaptionedFigure\" style=\"${widthStyle + float}\">${this.getFigureContent(buildSettings)}${captionFig}</figure>`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser linkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"linkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return `<a href=\"${clickLink}\" target=\"${target}\" ${className} ${id}><img src=\"${linkRelativeToCompileTarget}\" ${dimensionAttributes}loading=\"lazy\"></a>`\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `<div class=\"scrollYouTubeHolder\"><iframe class=\"scrollYouTubeEmbed\" src=\"${url}\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe></div>`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\ninlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractScrollParser\n cueFromId\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return `<script>/* ${this.content} */\\n${this.contents}</script>`\n }\n get contents() {\n return this.atoms.slice(1).map(filename => this.root.readFile(filename)).join(\";\\n\\n\")\n }\n buildJs() {\n return this.contents\n }\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType scriptAnyAtom\n javascript\n buildHtml() {\n return `<script>${this.scriptContent}</script>`\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return `<script>window.${varName} = ${this.root.readFile(this.filename)}</script>`\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<style>${style}</style><a class=\"keyboardNav doNotPrint\" style=\"left:.5rem;\" href=\"${linkToPrevious}\"><</a><a class=\"keyboardNav doNotPrint\" style=\"right:.5rem;\" href=\"${linkToNext}\">></a>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = `<script>document.addEventListener('keydown', function(event) {\n if (document.activeElement !== document.body) return\n if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return // dont interfere with keyboard back button shortcut\n const getLinks = () => document.getElementsByClassName(\"scrollKeyboardNav\")[0].getElementsByTagName(\"a\")\n if (event.key === \"ArrowLeft\")\n getLinks()[0].click()\n else if (event.key === \"ArrowRight\")\n getLinks()[1].click()\n });</script>`\n return `<div class=\"scrollKeyboardNav\" style=\"display:none;\"><a href=\"${linkToPrevious}\">${linkToPrevious}</a> · ${root.permalink} · <a href=\"${linkToNext}\">${linkToNext}</a>${script}</div>`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"<h></h>\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `<style>${css}</style><div id=\"scrollLeetSheet\">` + doc.map(obj => `<div class=\"${obj.category}\"><a href=\"${this.makeLink(obj.examples, obj.id)}\">${obj.isPopular ? \"<b>\" : \"\"}${obj.id}</a> ${obj.description}${obj.isPopular ? \"</b>\" : \"\"}${obj.note}</div>`).join(\"\\n\") + \"</div>\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n buildCsv() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category} = obj\n return {\n id,\n isPopular,\n description,\n popularity,\n category\n }\n })\n return new Particle(this.root.lodash.sortBy(rows, \"isPopular\")).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"<p><b>Parser Definition Parsers</b> define parsers that acquire, analyze and act on code.</p>\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"<p><b>Atom Definition Parsers</b> analyze the atoms in a line.</p>\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` <span class=\"note\">A${category.replace(\"Phase\", \"\").substr(1)}Time.</span>`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `<link rel=\"icon\" href=\"${favicon}\">` : \"\"\n const rssTag = rssFeedUrl ? `<link rel=\"alternate\" type=\"application/rss+xml\" title=\"${title}\" href=\"${rssFeedUrl}\">` : \"\"\n const gitTag = gitRepo ? `<link rel=\"source\" type=\"application/git\" title=\"Source Code Repository\" href=\"${gitRepo}\">` : \"\"\n return `<head>\n <meta charset=\"utf-8\">\n <title>${title}</title>\n <script>/* This HTML was generated by 📜 Scroll v${scrollVersion}. https://scroll.pub */</script>\n <style>@media print {.doNotPrint {display: none !important;}}</style>\n <link rel=\"canonical\" href=\"${canonicalUrl}\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <meta name=\"description\" content=\"${description}\">\n <meta name=\"generator\" content=\"Scroll v${scrollVersion}\">\n <meta property=\"og:title\" content=\"${title}\">\n <meta property=\"og:description\" content=\"${description}\">\n <meta property=\"og:image\" content=\"${openGraphImage}\">\n ${faviconTag}\n ${gitTag}\n ${rssTag}\n <meta name=\"twitter:card\" content=\"summary_large_image\">\n </head>\n <body>`\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `<blockquote class=\"scrollQuote\">${this.subparticlesToString()}</blockquote>`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return `<meta http-equiv=\"Refresh\" content=\"0; url='${this.getAtom(1)}'\" />`\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `<pre>${this.buildTxt()}</pre>`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n <script src=\"scripts/nested/hello.js\"></script>\n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`)\n }\n buildHtml() {\n return this.files.map(name => `<link rel=\"stylesheet\" type=\"text/css\" href=\"${name}\">`).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType anyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `</${this.closeTag}>` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nlinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an <a> tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cueFromId\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get attributes() {\n const attrs = [`href=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends linkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends linkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends linkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `<span class=\"scrollDateline\">${day} — </span>` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: `</${tag}>`, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom anyAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nchatLineParser\n popularity 0.009887\n catchAllAtomType anyAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(reduceParticle => {\n return {\n source: reduceParticle.getAtom(1),\n reduction: reduceParticle.getAtom(2),\n name: reduceParticle.getAtom(3) || reduceParticle.getAtomsFrom(1).join(\"_\")\n }\n })\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n if (reduction === \"concat\") {\n newRow[col.name] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[col.name] = group[0][sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[col.name] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom atomAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.getAtom(1)\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || untypedScalarValue === undefined) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return atom === \"\" || atom === undefined\n else if (operator === \"notEmpty\") return !(atom === \"\" || atom === undefined)\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n return this.getAtomsFrom(1)\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType stringAtom\n cue compose\n example\n table\n compose My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n catchAllAtomType stringAtom\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`<a href=\"${value.includes(\"@\") ? \"mailto:\" : \"\"}${value}\">${name}</a>`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.getAtom(1)\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom atomAtom\n catchAllAtomType atomAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType anyAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nscrollFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType scriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType anyAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nscrollFillParser\n cue fill\n extends abstractColumnNameParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nquoteLineParser\n popularity 0.004172\n catchAllAtomType anyAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return localStorage.getItem(fileOrUrl) || \"\"\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = localStorage.getItem(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n localStorage.setItem(url, await blob.text())\n return localStorage.getItem(url)\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `<a href=\"${this.editUrl}\" class=\"abstractTextLinkParser\">Edit</a>`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"162.0.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particle[methodName])\n .map((particle, index) => particle[methodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"<body>\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `<!DOCTYPE html>\\n<html lang=\"${this.lang}\">\\n${bodyTag}${content}\\n</body>\\n</html>` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll() {\n await this.load()\n await this.buildOne()\n await this.buildTwo()\n }\n async buildOne() {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne()\n }\n }\n async buildTwo(externalFilesCopied = {}) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(externalFilesCopied)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concept[measure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) row[measureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else row[measureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCache[measureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCache[measureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCache[measureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|footer$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"footer\")) {\n const pushes = particle.getParticles(\"footer\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` <item>\n <title>${title}</title>\n <link>${canonicalUrl}</link>\n <pubDate>${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}</pubDate>\n </item>`\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType anyAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n description Table from inline delimited data.\n catchAllAtomType anyAtom\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
\ No newline at end of file
+const AppConstants = {"parsers":"columnNameAtom\n extends stringAtom\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n extends stringAtom\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint comment\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n extends anyAtom\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\nscriptAnyAtom\n extends codeAtom\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom cueAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint string\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\njavascriptAtom\n extends stringAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\"<a \") ? false : true\n return shouldLinkify ? this.replaceNotes(Utils.linkify(originalText)) : originalText\n }\n replaceNotes(originalText) {\n // Skip the replacements if there are no footnotes or the text has none.\n if (!this.root.footnotes.length || !originalText.includes(\"^\")) return originalText\n this.root.footnotes.forEach((note, index) => {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `<a href=\"#${note.htmlId}\" class=\"scrollNoteLink\" id=\"${linkBack}\"><sup>${note.label}</sup></a>`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.text}${this.closingTag}`\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return `</${tag}>`\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `<span class=\"scrollBlink\">${super.buildHtml()}</span>\n <script>setInterval(()=>{ Array.from(document.getElementsByClassName(\"scrollBlink\")).forEach(el => el.style.visibility = el.style.visibility === \"hidden\" ? \"visible\" : \"hidden\") }, 500)</script>`\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"linkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"</center>\")\n return `<center>${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser\n javascript\n compileSubparticles() {\n return this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildHtml() {\n return super.buildHtml() + this.compileSubparticles()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `<div style=\"text-indent:${(this.getIndentLevel() - 1) * 20}px;\"><input type=\"checkbox\" ${this.checked} id=\"${this.id}\"><label for=\"${this.id}\">` + super.text + `</label></div>`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `</${listType}>` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return `<style>\\n.${this.constructor.name} li::marker {content: \"${this.cue} \";}\\n</style>`\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* <span id=\"counter${id}\" title=\"0\">0</span><script>setInterval(()=>{ const el = document.getElementById('counter${id}'); el.title = parseFloat(el.title) + ${increment}; el.textContent = Math.floor(parseFloat(el.title)).toLocaleString()}, 100)</script> ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"</details>\")\n return `<details>${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `<a class=\"scrollFootNoteUsageLink\" href=\"#noteUsage${this.noteDefinitionIndex}\">${this.label}</a> ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"</div>\")\n return `<div class=\"scrollSection\">` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}></${this.tag}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return `<audio controls ${this.getAsHtmlAttributes(\"loop autoplay\".split(\" \"))}><source src=\"${this.filename}\" type=\"audio/mpeg\"></audio>`\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom integerAtom\n heightParser\n cueFromId\n atoms cueAtom integerAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* <span class=\"scrollStopwatchParser\" id=\"stopwatch${id}\">0.0</span><script>{let startTime = parseFloat(new URLSearchParams(window.location.search).get('start') || 0); document.getElementById('stopwatch${id}').title = startTime; setInterval(()=>{ const el = document.getElementById('stopwatch${id}'); el.title = parseFloat(el.title) + .1; el.textContent = (parseFloat(el.title)).toFixed(1)}, 100)}</script> `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"</div>\") // Single columns are self-closing after section break.\n return stackContents + `<div class=\"scrollColumns\" style=\"column-width:${columnWidth}ch;column-count:${maxColumns};max-width:${maxTotalWidth}ch;\">`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"</div>\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"</div>\")\n return `<style>.scrollContainerParser{width: 100%; box-sizing: border-box; max-width: ${this.maxWidth}; margin: 0 auto;}</style>` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `<div class=\"${this.defaultClass}\"><span>${this.dinkus}</span></div>`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n javascript\n buildHtml() {\n return `<hr>`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `<style>.abstractIconButtonParser {position:absolute;top:0.25rem; }.abstractIconButtonParser svg {fill: rgba(204,204,204,.8);width:1.875rem;height:1.875rem; padding: 0 7px;} .abstractIconButtonParser:hover svg{fill: #333;}</style><a href=\"${this.link}\" class=\"doNotPrint abstractIconButtonParser\" style=\"${this.style}\">${this.svg}</a>`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg <svg fill=\"#000000\" xmlns=\"http://www.w3.org/2000/svg\" width=\"800px\" height=\"800px\" viewBox=\"0 0 52 52\" enable-background=\"new 0 0 52 52\" xml:space=\"preserve\"><path d=\"M38.6,20.4c-1-6.5-6.7-11.5-13.5-11.5c-7.6,0-13.7,6.1-13.7,13.7c0,0.3,0,0.7,0.1,1c-5,0.4-8.9,4.6-8.9,9.6 c0,5.4,4.3,9.7,9.7,9.7h11.5c-0.8-0.8-8.1-8.1-8.1-8.1c-0.4-0.4-0.4-0.9,0-1.3l1.3-1.3c0.4-0.4,0.9-0.4,1.3,0l3.5,3.5 c0.4,0.4,1.1,0.1,1.1-0.4V21.8c0-0.4,0.5-0.9,1-0.9h1.9c0.5,0,0.9,0.4,0.9,0.9v13.4c0,0.6,0.8,0.8,1.1,0.4l3.5-3.5 c0.4-0.4,0.9-0.4,1.3,0l1.3,1.3c0.4,0.4,0.4,0.9,0,1.3L26,42.9h12.3v0c6.1-0.1,11-5.1,11-11.3C49.4,25.5,44.6,20.6,38.6,20.4z\"/></svg><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->\n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg <svg width=\"800px\" height=\"800px\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M21.1213 2.70705C19.9497 1.53548 18.0503 1.53547 16.8787 2.70705L15.1989 4.38685L7.29289 12.2928C7.16473 12.421 7.07382 12.5816 7.02986 12.7574L6.02986 16.7574C5.94466 17.0982 6.04451 17.4587 6.29289 17.707C6.54127 17.9554 6.90176 18.0553 7.24254 17.9701L11.2425 16.9701C11.4184 16.9261 11.5789 16.8352 11.7071 16.707L19.5556 8.85857L21.2929 7.12126C22.4645 5.94969 22.4645 4.05019 21.2929 2.87862L21.1213 2.70705ZM18.2929 4.12126C18.6834 3.73074 19.3166 3.73074 19.7071 4.12126L19.8787 4.29283C20.2692 4.68336 20.2692 5.31653 19.8787 5.70705L18.8622 6.72357L17.3068 5.10738L18.2929 4.12126ZM15.8923 6.52185L17.4477 8.13804L10.4888 15.097L8.37437 15.6256L8.90296 13.5112L15.8923 6.52185ZM4 7.99994C4 7.44766 4.44772 6.99994 5 6.99994H10C10.5523 6.99994 11 6.55223 11 5.99994C11 5.44766 10.5523 4.99994 10 4.99994H5C3.34315 4.99994 2 6.34309 2 7.99994V18.9999C2 20.6568 3.34315 21.9999 5 21.9999H16C17.6569 21.9999 19 20.6568 19 18.9999V13.9999C19 13.4477 18.5523 12.9999 18 12.9999C17.4477 12.9999 17 13.4477 17 13.9999V18.9999C17 19.5522 16.5523 19.9999 16 19.9999H5C4.44772 19.9999 4 19.5522 4 18.9999V7.99994Z\"/></svg>\n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg <svg viewBox=\"3 5 24 20\" width=\"24\" height=\"20\" xmlns=\"http://www.w3.org/2000/svg\"><g transform=\"matrix(1, 0, 0, 1, 0, -289.0625)\"><path style=\"opacity:1;stroke:none;stroke-width:0.49999997;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1\" d=\"M 5 5 C 4.2955948 5 3.6803238 5.3628126 3.3242188 5.9101562 L 14.292969 16.878906 C 14.696939 17.282876 15.303061 17.282876 15.707031 16.878906 L 26.675781 5.9101562 C 26.319676 5.3628126 25.704405 5 25 5 L 5 5 z M 3 8.4140625 L 3 23 C 3 24.108 3.892 25 5 25 L 25 25 C 26.108 25 27 24.108 27 23 L 27 8.4140625 L 17.121094 18.292969 C 15.958108 19.455959 14.041892 19.455959 12.878906 18.292969 L 3 8.4140625 z \" transform=\"translate(0,289.0625)\" id=\"rect4592\"/></g></svg>\n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg <svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12.7166 3.79541C12.2835 3.49716 11.7165 3.49716 11.2834 3.79541L4.14336 8.7121C3.81027 8.94146 3.60747 9.31108 3.59247 9.70797C3.54064 11.0799 3.4857 13.4824 3.63658 15.1877C3.7504 16.4742 4.05336 18.1747 4.29944 19.4256C4.41371 20.0066 4.91937 20.4284 5.52037 20.4284H8.84433C8.98594 20.4284 9.10074 20.3111 9.10074 20.1665V15.9754C9.10074 14.9627 9.90433 14.1417 10.8956 14.1417H13.4091C14.4004 14.1417 15.204 14.9627 15.204 15.9754V20.1665C15.204 20.3111 15.3188 20.4284 15.4604 20.4284H18.4796C19.0806 20.4284 19.5863 20.0066 19.7006 19.4256C19.9466 18.1747 20.2496 16.4742 20.3634 15.1877C20.5143 13.4824 20.4594 11.0799 20.4075 9.70797C20.3925 9.31108 20.1897 8.94146 19.8566 8.7121L12.7166 3.79541ZM10.4235 2.49217C11.3764 1.83602 12.6236 1.83602 13.5765 2.49217L20.7165 7.40886C21.4457 7.91098 21.9104 8.73651 21.9448 9.64736C21.9966 11.0178 22.0564 13.5119 21.8956 15.3292C21.7738 16.7067 21.4561 18.4786 21.2089 19.7353C20.9461 21.0711 19.7924 22.0001 18.4796 22.0001H15.4604C14.4691 22.0001 13.6655 21.1791 13.6655 20.1665V15.9754C13.6655 15.8307 13.5507 15.7134 13.4091 15.7134H10.8956C10.754 15.7134 10.6392 15.8307 10.6392 15.9754V20.1665C10.6392 21.1791 9.83561 22.0001 8.84433 22.0001H5.52037C4.20761 22.0001 3.05389 21.0711 2.79113 19.7353C2.54392 18.4786 2.22624 16.7067 2.10437 15.3292C1.94358 13.5119 2.00338 11.0178 2.05515 9.64736C2.08957 8.73652 2.55427 7.91098 3.28346 7.40886L10.4235 2.49217Z\"/></svg>\n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg <svg xmlns=\"http://www.w3.org/2000/svg\" direction=\"ltr\" width=\"231.52568231268793\" height=\"249.33156169975996\" viewBox=\"297.27169239534896 1273.121785992385 231.52568231268793 249.33156169975996\" stroke-linecap=\"round\" stroke-linejoin=\"round\" data-color-mode=\"dark\" class=\"tl-container tl-theme__force-sRGB tl-theme__dark\" ><defs/><g transform=\"matrix(1, 0, 0, 1, 395.9682, 1413.3618)\" opacity=\"1\"><g transform=\"scale(1)\"><path d=\"M-5.7342,-1.1484 T-5.0182,-4.6536 -2.6672,-12.9768 1.7374,-22.3903 8.4597,-30.0892 16.5455,-35.2796 25.3478,-37.8247 34.4641,-37.2199 43.0676,-33.4606 50.27,-27.1118 55.7861,-19.7972 59.8042,-11.6764 61.6844,-2.3157 60.8049,7.9615 57.3259,17.5954 51.2605,25.5515 41.9451,31.9237 30.6489,36.2084 18.9812,37.6038 8.4915,36.637 -1.3063,34.5174 -10.6869,31.6604 -19.0463,27.0252 -26.0612,20.1562 -31.864,11.1271 -35.2772,1.0066 -35.7963,-9.1933 -35.203,-19.2337 -32.52,-28.3259 -27.3388,-37.4245 -20.2857,-45.7085 -11.787,-53.0395 -2.4314,-58.4864 7.1724,-61.8739 17.5002,-65.6025 28.2859,-68.1034 38.514,-69.1081 48.1844,-69.9134 57.9136,-70.1787 67.4056,-69.5971 76.297,-67.7446 84.5233,-63.97 92.0158,-57.5003 98.1976,-48.5614 102.8839,-38.056 105.9936,-27.6097 107.074,-18.168 107.175,-8.2882 106.6452,1.9768 105.4734,11.7698 104.0279,21.1013 101.7439,31.0445 98.0977,40.6386 92.7822,49.1039 86.2847,57.2337 78.6099,64.7113 69.8339,70.974 61.2863,75.092 52.4283,78.2186 42.5338,80.7945 32.7733,82.6629 22.8568,83.6574 12.2803,83.3538 2.4971,81.4828 -6.3367,78.789 -15.1061,75.8011 -24.259,72.3779 -33.2286,68.991 -41.5707,64.8363 -49.153,59.1909 -55.8375,52.2938 -62.9311,43.9553 -68.4604,34.4882 -71.2761,23.5855 -72.748,12.7452 -73.7673,2.0453 -74.1187,-8.7386 -72.293,-19.3856 -68.7376,-30.4053 -64.12,-39.7852 -59.1825,-47.8931 -53.7695,-56.4512 -48.1998,-65.7676 -43.1389,-74.401 -36.9006,-81.0179 -28.4317,-86.2639 -18.3342,-90.3521 -8.9259,-93.9355 0.1927,-98.7179 9.3551,-103.3537 18.7651,-106.7552 29.7482,-110.3584 40.9287,-113.446 50.87,-115.2231 57.8279,-116.0074 60.4065,-116.0632 A7.8326,7.8326 0 0 1 61.1735,-100.4168 T58.6018,-100.2201 51.1634,-99.3008 41.695,-96.884 32.4044,-94.0187 22.65,-91.382 13.4974,-87.3756 5.0519,-83.1026 -5.0443,-78.6175 -15.7236,-74.4743 -24.6193,-70.2681 -31.274,-62.5903 -36.3827,-53.9362 -41.2833,-46.2905 -46.0679,-38.5161 -50.639,-30.6576 -54.6876,-22.4063 -57.6038,-12.3346 -58.5104,-1.8678 -57.6022,9.4969 -56.0628,20.477 -53.6434,29.0487 -48.8908,36.7517 -42.4075,43.9784 -35.3064,50.6236 -27.3553,55.2062 -17.6884,59.4064 -7.6442,63.6638 2.4427,67.0449 12.7308,69.4843 22.9294,70.2171 33.2547,69.3921 42.4972,67.7384 52.2753,64.8425 61.8649,60.9142 69.5292,56.0828 76.3604,49.5927 82.431,42.3194 87.219,34.619 90.6076,25.5217 92.547,16.0352 93.8703,7.1315 94.4972,-2.5268 94.3672,-13.3681 93.3204,-24.4252 90.5377,-34.1656 85.7321,-43.6441 78.7334,-51.8057 68.7542,-56.0559 57.9857,-57.2713 48.079,-57.0198 38.4535,-56.3204 28.5246,-55.0036 18.6104,-52.4796 9.6402,-49.559 0.7193,-46.2981 -7.1531,-41.3715 -13.708,-34.8957 -19.5435,-26.853 -22.8356,-17.0622 -23.363,-6.5446 -21.9969,3.1439 -17.1086,11.9113 -9.276,18.744 -0.0685,22.6058 9.3636,24.796 19.2422,25.8817 29.052,25.0411 37.9338,21.3502 45.6819,13.8498 49.6675,4.5228 49.2802,-5.1854 45.3277,-14.4455 38.8603,-21.7879 30.5488,-25.7753 21.3356,-24.3975 13.6687,-19.2391 8.8217,-10.9919 6.423,-2.3609 5.7342,1.1484 A5.8481,5.8481 0 0 1 -5.7342,-1.1484 Z\" stroke-linecap=\"round\"/></g></g></svg>\n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return `<div class=\"abstractTextLinkParser\"><a href=\"${this.link}\">${this.text}</a></div>`\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n <script>\n sendFormViaEmail = form => {\n const mailto = new URL(\"mailto:\")\n const params = []\n const { value, title } = form.querySelector('button[type=\"submit\"]')\n params.push(`subject=${encodeURIComponent(value)}`)\n params.push(`to=${encodeURIComponent(title)}`)\n const oneTextarea = form.querySelector('textarea[title=\"oneTextarea\"]')\n const body = oneTextarea ? codeMirrorInstance.getValue() : Array.from(new FormData(form)).map(([name, value]) => `${name} ${value}`).join(\"\\\\n\")\n params.push(`body=${encodeURIComponent(body)}`)\n mailto.search = params.join(\"&\")\n window.open(mailto.href, '_blank')\n }\n </script>\n string style\n <style> .scrollFormParser {\n font-family: \"Gill Sans\", \"Bitstream Vera Sans\", sans-serif;\n }\n .scrollFormParser input , .scrollFormParser textarea{\n padding: 10px;\n margin-bottom: 10px;\n width: 100%;\n box-sizing: border-box;\n } .scrollFormParser label {\n display: block;\n margin-bottom: 5px;\n }\n </style>\n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = `<textarea placeholder=\"${placeholder}\" id=\"${Name}\" name=\"${Name}\" ${IsRequired ? \"required\" : \"\"}></textarea>`\n else\n tag = `<input placeholder=\"${placeholder}\" type=\"${type}\" id=\"${Name}\" name=\"${Name}\" ${IsRequired ? \"required\" : \"\"}>`\n return `<div><label for=\"${Name}\" title=\"${IsRequired ? \"Required\" : \"\"}\">${ucFirst}${IsRequired ? \"*\" : \"\"}:</label>${tag}</div>`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}<form ${isEmail ? \"onsubmit='sendFormViaEmail(this); return false;'\" : ` method='post' action='${formDestination}'`} class=\"scrollFormParser\">${this.inputs}<button value=\"${subject}\" title=\"${formDestination}\" class=\"scrollButton\" type=\"submit\">${callToAction}</button>${this.footer}</form>`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n <link rel=\"stylesheet\" href=\".codeMirror.css\">\n <script src=\".scrollLibs.js\"></script>\n <script src=\".constants.js\"></script>\n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `<textarea title=\"oneTextarea\" rows=\"${Math.min(this.root.measures.length * 2, 30)}\" placeholder=\"${this.placeholder}\" id=\"${Name}\" name=\"${Name}\"></textarea>\n <script id=\"${Name}Parsers\" type=\"text/plain\">${this.parsersBundle}</script>\n <script>{\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n let {width, height} = document.getElementById('${Name}').getBoundingClientRect();\n const sp = new Particle(AppConstants.parsers)\n sp.filter(particle => particle.getLine().match(parserRegex)).forEach(part => {\n part.appendLine(\"boolean suggestInAutocomplete false\")\n })\n const customScrollParser = new HandParsersProgram(sp.toString() + document.getElementById(\"${Name}Parsers\").textContent).compileAndReturnRootParser()\n codeMirrorInstance = new ParsersCodeMirrorMode(\"custom\", () => customScrollParser, undefined, CodeMirror).register().fromTextAreaWithAutocomplete(document.getElementById(\"${Name}\"), {\n lineWrapping: false,\n lineNumbers: false\n })\n codeMirrorInstance.setSize(width, height);\n codeMirrorInstance.setValue(\\`${this.value}\\`); }</script>`\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `<a class=\"scrollContinueReadingLink\" href=\"${linkRelativeToCompileTarget}\">Continue reading...</a>`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `<div class=\"scrollSnippetContainer\">${this.makeSnippet(file.file.scrollProgram, buildSettings)}</div>`\n }).join(\"\\n\\n\")\n return `<div class=\"scrollColumns\" style=\"column-width:35ch;\">${snippets}</div>`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue nav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return `<nav class=\"scrollNavParser\">` + this.root.getFilesByTags(this.content).map(file => {\n const { linkTitle, permalink } = file.file.scrollProgram\n return `<a href=\"${permalink}\">${linkTitle}</a>` \n }).join(this.get(\"join\") || \" \") + `</nav>`\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `<div><a href=\"${permalink}\">${title}</a><div>${description}...</div><div class=\"subdued\" style=\"text-align:right;\">${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}</div></div>`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `<div id=\"${id}\" class=\"scrollNoticesParser\" style=\"display:none;\"></div><script>(function(){\n const params = new URLSearchParams(window.location.search)\n if (!params.size) return\n document.getElementById('${id}').innerHTML = Array.from(params.entries()).map(([key, value]) => {\n if (key === \"error\") \n return '<div style=\"color:red\">Error: ' + value + '</div>'\n if (key === \"success\")\n return '<div style=\"color:green\">Success: ' + value + '</div>'\n return '<div>' + key + ': ' + value + '</div>'\n }).join(\"\") || \"No query parameters found\"\n document.getElementById('${id}').style.display = \"block\"\n })()</script>`\n }\nprintSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.buildTxt().replace(/\\</g, \"<\")}</code>`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual} = this\n const expected = this.subparticlesToString()\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType anyAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n javascript\n buildHtml() {\n return `<div class=\"printDateParser\">${this.day}</div>`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne() { await this._buildFileType(this.extension) }\nbuildCsvParser\n popularity 0.000096\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo() {\n await this._buildFileType(this.extension)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(externalFilesCopied) {\n await this._copyExternalFiles(externalFilesCopied)\n await super.buildTwo()\n }\n async _copyExternalFiles(externalFilesCopied = {}) {\n if (!this.isNodeJs()) return\n const {root} = this\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Write RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Write concepts to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom anyAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildCommandParser\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Write measures to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom anyAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo() {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${this.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the <html lang=\"\"> tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Meta tag description.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType anyAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType anyAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `<div style=\"text-align: ${index % 2 ? \"right\" : \"left\"};\" class=\"scrollChat ${index % 2 ? \"scrollChatRight\" : \"scrollChatLeft\"}\"><span>${line.asString}</span></div>` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = typeof d3 === \"undefined\" ? require('d3') : d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = typeof d3 === \"undefined\" ? require('d3') : d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/</g, \"<\")\n return {\n title, titleLink: relativePath + permalink, text, date, wordCount, minutes\n }\n }\n columnNames = \"title titleLink text date wordCount minutes\".split(\" \")\nscrollPostsParser\n popularity 0.000024\n cue posts\n description Posts as table.\n extends abstractPostsParser\n example\n // Print a search table:\n posts\n printTable\n tableSearch\n // Dump a CSV of blog:\n buildHtml\n buildCsv\n buildJson\nscrollPostsMetaParser\n popularity 0.000024\n cue postsMeta\n description Post meta as table.\n extends abstractPostsParser\n javascript\n columnNames = [\"date\", \"year\", \"title\", \"permalink\", \"authors\", \"tags\", \"wordCount\", \"minutes\"]\n postToRow(file) {\n const {date, year, title, permalink, authors, tags, wordCount, minutes} = file.file.scrollProgram\n return {\n date, year, title, permalink, authors, tags, wordCount, minutes\n }\n }\nprintFeedParser\n popularity 0.000048\n description Print group to RSS.\n extends abstractPostsParser\n example\n printFeed index\n printFeed cars/index\n buildRss\n javascript\n buildRss() {\n const {dayjs} = this.root\n const scrollPrograms = this.files.map(file => file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>\n <rss version=\"2.0\">\n <channel>\n <title>${title}</title>\n <link>${baseUrl}</link>\n <description>${description}</description>\n <lastBuildDate>${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}</lastBuildDate>\n <language>en-us</language>\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n </channel>\n </rss>`\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.code.replace(/\\</g, \"<\")}</code>`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `<div class=\"codeWithHeader\"><div class=\"codeHeader\">${this.content}</div>${super.buildHtml()}</div>`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n <script>\n document.addEventListener(\"DOMContentLoaded\", () => document.querySelectorAll(\".scrollCodeBlock\").forEach(block =>\n {\n if (!navigator.clipboard) return\n const button = document.createElement(\"span\")\n button.classList.add(\"scrollCopyButton\")\n block.appendChild(button)\n button.addEventListener(\"click\", async () => {\n await navigator.clipboard.writeText(block.innerText)\n button.classList.add(\"scrollCopiedButton\")\n })\n }\n ))\n </script>\nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = `<style>\n .heatrixContainer {\n margin: auto;\n }.heatrixRow {white-space: nowrap;}\n ${cssId} .heatrixAtom {\n font-family: arial;\n border-radius: 2px;\n border: 1px solid transparent;\n display: inline-block;\n margin: 1px;\n text-align: center;\n vertical-align: middle;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n .heatrixAtom a {\n color: black;\n }\n ${cssId} .heatrixAtom{\n width: ${defaultWidth};\n height: ${defaultHeight};\n font-size: ${fontSize};\n line-height: ${lineHeight};\n }\n </style>`\n const firstRow = this.table[0]\n return (\n `<div class=\"heatrixContainer\" id=\"heatrix${this.uid}\">${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `<div class=\"heatrixRow heatrixRow${rowIndex}\">${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return `<div class=\"heatrixAtom heatrixColumn${columnIndex}${valueClass}\" style=\"${inlineStyle}\"><a title=\"${label}\" ${href}>${label}</a></div>`\n })\n .join(\"\")}</div>`\n })\n .join(\"\\n\") +\n \"</div>\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom anyAtom\n cueFromId\n single\n colorParser\n atoms cueAtom anyAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType anyAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n <link rel=\"stylesheet\" href=\".leaflet.css\">\n <script src=\".leaflet.js\"></script>\n <script src=\".scrollLibs.js\"></script>\n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `<div id=\"${mapId}\" style=\"${style}\"></div>\n <script>\n {\n if (!window.maps) window.maps = {}\n const moveToMyLocation = () => {\n if (!navigator.geolocation) return\n navigator.geolocation.getCurrentPosition((position) => {\n const { latitude, longitude } = position.coords\n window.maps.${mapId}.setView([latitude, longitude])\n L.circleMarker([latitude, longitude], {\n fillOpacity: 0.8,\n radius: 20,\n weight: 4\n })\n .addTo(window.maps.${mapId})\n }, () => {})\n }\n const lat = ${this.get(\"lat\") ?? 37.8}\n const long = ${this.get(\"long\") ?? 4}\n if (${this.has(\"geolocate\")}){\n moveToMyLocation()\n }\n const zoomLevel = ${this.get(\"zoom\") ?? 4}\n const hover = '${this.get(\"hover\") || \"<b>{title}</b><br>{description}\"}'\n const template = ${JSON.stringify(template)}\n const points = ${JSON.stringify((this.parent.coreTable || []).filter(point => point.lat && point.long), undefined, 2)}\n window.maps.${mapId} = L.map(\"map${id}\").setView([lat, long], zoomLevel)\n const map = window.maps.${mapId}\n const tileOptions = {\n \"default\": {\n baseLayer: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',\n attribution: '<a href=\"https://www.openstreetmap.org/\">OpenStreetMap</a> contributors'\n },\n light: {\n baseLayer: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',\n attribution: '<a href=\"https://www.openstreetmap.org/\">OpenStreetMap</a> contributors <a href=\"https://carto.com/ attributions\">CARTO</a>'\n },\n }\n const {baseLayer, attribution} = tileOptions.${this.get(\"tiles\") || \"default\"}\n L.tileLayer(baseLayer, {\n attribution,\n maxZoom: 19\n }).addTo(map);\n points.forEach(point => {\n L.circleMarker([point.lat, point.long], {...template, ...Object.fromEntries(\n Object.entries(point).filter(([key, value]) => value !== null)\n )})\n .addTo(map)\n .bindPopup(new Particle(point).evalTemplateString(hover))\n })\n }\n </script>`\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n <script src=\".d3.js\"></script>\n <script src=\".plot.js\"></script>\n example\n plot\n inScope abstractColumnNameParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `<div id=\"${id}\"></div><script>\n {\n let loadChart = async () => {\n const data = ${this.dataCode}\n const get = (col, index ) => col !== \"undefined\" ? col : (index === undefined ? undefined : Object.keys(data[0])[index])\n document.querySelector(\"#${id}\").append(Plot.plot(${this.plotOptions}))\n }\n loadChart()\n }\n </script>`\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n }`\n }\nscatterplotParser\n extends abstractPlotParser\n description Scatterplot Widget.\n // todo: make copyFromExternal work with inheritance\n string copyFromExternal .d3.js .plot.js\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true,\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce <script src=\".sparkline.js\"></script>\n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return `<span id=\"${id}\"></span><script>new Sparkline(document.getElementById(\"${id}\"), {dotRadius: 0, width: ${width}, height: ${height}, lineColor: \"${lineColor}\", tooltip: (value,index) => ${start} + index + \": \" + value}).draw(${JSON.stringify(columnValues)})</script>`\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `<th>${column.name}</th>\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `<a href=\"${link}\">${text}</a>`\n else if (col.name.endsWith(\"Url\")) tagged = `<a href=\"${content}\">${col.name.replace(\"Url\", \"\")}</a>`\n else if (isUrl) tagged = `<a href=\"${content}\">${text}</a>`\n str += `<td>${tagged}</td>\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `<tr>${this.toRow(row)}</tr>`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `<table id=\"table${this._getUid()}\" class=\"scrollTable\">\n <thead><tr>${this.tableHeader.join(\"\\n\")}</tr></thead>\n <tbody>${this.tableBody}</tbody>\n </table>`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n <link rel=\"stylesheet\" href=\".katex.min.css\">\n <script defer src=\".katex.min.js\"></script>\n <script>\n document.addEventListener(\"DOMContentLoaded\", () => document.querySelectorAll(\".scrollKatex\").forEach(el =>\n {\n katex.render(el.innerText, el, {\n throwOnError: false\n });\n }\n ))\n </script>\n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `<div class=\"scrollKatex\" id=\"${id}\">${content + this.subparticlesToString()}</div>`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `<style>#helpfulNotFound{margin: 100px 0;}</style><h1 id=\"helpfulNotFound\"></h1><script defer src=\"/.helpfulNotFound.js\"></script><script>document.addEventListener(\"DOMContentLoaded\", () => new NotFoundApp('${this.content}'))</script>`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `<style>html {font-size: var(--scrollBaseFontSize, 28px);} body {margin: auto; width: 500px;}.slideshowNav{text-align: center; margin-bottom:20px; font-size: 24px;color: rgba(204,204,204,.8);} a{text-decoration: none; color: rgba(204,204,204,.8);}</style><script defer src=\".jquery-3.7.1.min.js\"></script><div class=\"slideshowNav\"></div><script defer src=\".slideshow.js\"></script>`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n <script defer src=\".jquery-3.7.1.min.js\"></script>\n <style>.dt-search{font-family: \"SF Pro\", \"Helvetica Neue\", \"Segoe UI\", \"Arial\";}</style>\n <link rel=\"stylesheet\" href=\".datatables.css\">\n <script defer src=\".datatables.js\"></script>\n <script defer src=\".dayjs.min.js\"></script>\n <script defer src=\".tableSearch.js\"></script>\n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>${this.css}</style>`\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCs() {\n return this.css\n }\ninlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractScrollParser\n cueFromId\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return `<style>/* ${this.content} */\\n${this.css}</style>`\n }\n get css() {\n return this.atoms.slice(1).map(filename => this.root.readFile(filename)).join(\"\\n\\n\")\n }\n buildCs() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>html, body { background: ${this.content};}</style>`\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>html, body { color: ${this.content};}</style>`\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return `<style>html, body, h1,h2,h3 { font-family: ${font};}</style>`\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return `<link rel=\"stylesheet\" type=\"text/css\" href=\"${this.filename}\">`\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return `<script src=\"${this.filename}\"></script>`\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `<tr>` + items.map(particle => `<td>${particle.cue}<span>${particle.content}</span></td>`).join(\"\\n\") + `</tr>\\n`\n }\n buildHtml() {\n return `<table class=\"scrollDashboard\">${this.tableBody}</table>`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = this[method]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = next[method]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.code.replace(/\\</g, \"<\")}</code>`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType anyAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\ninspectBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .inspector.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `<div class=\"inspectorParticle\"><span class=\"inspectorParticleId\">${particle.constructor.name}</span>${particle.atoms.map((atom, index) => `<span class=\"inspectorAtom\">${atom}<span class=\"inspectorAtomType\">${atomTypes[index]}</span></span>`).join(\" \")}${(particle.length ? `<br><div class=\"inspectorSubparticles\">` + particle.map(mapFn).join(\"<br>\") + `</div>` : \"\")}</div>`}\n return this.selectedParticles.map(mapFn).join(\"<br>\")\n }\n buildHtml() {\n return `<link rel=\"stylesheet\" href=\".inspector.css\">` + this.code\n }\ninspectAboveParser\n description Inspect particle above.\n extends inspectBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return `<style>${this.css}</style>`\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCs() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(`</${tag}>`)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `<br>`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => `<iframe src=\"${url}\" frameborder=\"0\"></iframe>`).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `<figcaption>${caption.buildHtml()}</figcaption>` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `<figure class=\"scrollCaptionedFigure\" style=\"${widthStyle + float}\">${this.getFigureContent(buildSettings)}${captionFig}</figure>`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser linkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"linkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return `<a href=\"${clickLink}\" target=\"${target}\" ${className} ${id}><img src=\"${linkRelativeToCompileTarget}\" ${dimensionAttributes}loading=\"lazy\"></a>`\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `<div class=\"scrollYouTubeHolder\"><iframe class=\"scrollYouTubeEmbed\" src=\"${url}\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe></div>`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\ninlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractScrollParser\n cueFromId\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return `<script>/* ${this.content} */\\n${this.contents}</script>`\n }\n get contents() {\n return this.atoms.slice(1).map(filename => this.root.readFile(filename)).join(\";\\n\\n\")\n }\n buildJs() {\n return this.contents\n }\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType scriptAnyAtom\n javascript\n buildHtml() {\n return `<script>${this.scriptContent}</script>`\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return `<script>window.${varName} = ${this.root.readFile(this.filename)}</script>`\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<style>${style}</style><a class=\"keyboardNav doNotPrint\" style=\"left:.5rem;\" href=\"${linkToPrevious}\"><</a><a class=\"keyboardNav doNotPrint\" style=\"right:.5rem;\" href=\"${linkToNext}\">></a>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = `<script>document.addEventListener('keydown', function(event) {\n if (document.activeElement !== document.body) return\n if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return // dont interfere with keyboard back button shortcut\n const getLinks = () => document.getElementsByClassName(\"scrollKeyboardNav\")[0].getElementsByTagName(\"a\")\n if (event.key === \"ArrowLeft\")\n getLinks()[0].click()\n else if (event.key === \"ArrowRight\")\n getLinks()[1].click()\n });</script>`\n return `<div class=\"scrollKeyboardNav\" style=\"display:none;\"><a href=\"${linkToPrevious}\">${linkToPrevious}</a> · ${root.permalink} · <a href=\"${linkToNext}\">${linkToNext}</a>${script}</div>`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"<h></h>\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `<style>${css}</style><div id=\"scrollLeetSheet\">` + doc.map(obj => `<div class=\"${obj.category}\"><a href=\"${this.makeLink(obj.examples, obj.id)}\">${obj.isPopular ? \"<b>\" : \"\"}${obj.id}</a> ${obj.description}${obj.isPopular ? \"</b>\" : \"\"}${obj.note}</div>`).join(\"\\n\") + \"</div>\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n buildCsv() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category} = obj\n return {\n id,\n isPopular,\n description,\n popularity,\n category\n }\n })\n return new Particle(this.root.lodash.sortBy(rows, \"isPopular\")).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"<p><b>Parser Definition Parsers</b> define parsers that acquire, analyze and act on code.</p>\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"<p><b>Atom Definition Parsers</b> analyze the atoms in a line.</p>\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` <span class=\"note\">A${category.replace(\"Phase\", \"\").substr(1)}Time.</span>`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `<link rel=\"icon\" href=\"${favicon}\">` : \"\"\n const rssTag = rssFeedUrl ? `<link rel=\"alternate\" type=\"application/rss+xml\" title=\"${title}\" href=\"${rssFeedUrl}\">` : \"\"\n const gitTag = gitRepo ? `<link rel=\"source\" type=\"application/git\" title=\"Source Code Repository\" href=\"${gitRepo}\">` : \"\"\n return `<head>\n <meta charset=\"utf-8\">\n <title>${title}</title>\n <script>/* This HTML was generated by 📜 Scroll v${scrollVersion}. https://scroll.pub */</script>\n <style>@media print {.doNotPrint {display: none !important;}}</style>\n <link rel=\"canonical\" href=\"${canonicalUrl}\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <meta name=\"description\" content=\"${description}\">\n <meta name=\"generator\" content=\"Scroll v${scrollVersion}\">\n <meta property=\"og:title\" content=\"${title}\">\n <meta property=\"og:description\" content=\"${description}\">\n <meta property=\"og:image\" content=\"${openGraphImage}\">\n ${faviconTag}\n ${gitTag}\n ${rssTag}\n <meta name=\"twitter:card\" content=\"summary_large_image\">\n </head>\n <body>`\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `<blockquote class=\"scrollQuote\">${this.subparticlesToString()}</blockquote>`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return `<meta http-equiv=\"Refresh\" content=\"0; url='${this.getAtom(1)}'\" />`\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `<pre>${this.buildTxt()}</pre>`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n <script src=\"scripts/nested/hello.js\"></script>\n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`)\n }\n buildHtml() {\n return this.files.map(name => `<link rel=\"stylesheet\" type=\"text/css\" href=\"${name}\">`).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType anyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `</${this.closeTag}>` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nlinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an <a> tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cueFromId\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get attributes() {\n const attrs = [`href=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends linkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends linkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends linkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `<span class=\"scrollDateline\">${day} — </span>` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: `</${tag}>`, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom anyAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nchatLineParser\n popularity 0.009887\n catchAllAtomType anyAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(reduceParticle => {\n return {\n source: reduceParticle.getAtom(1),\n reduction: reduceParticle.getAtom(2),\n name: reduceParticle.getAtom(3) || reduceParticle.getAtomsFrom(1).join(\"_\")\n }\n })\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n if (reduction === \"concat\") {\n newRow[col.name] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[col.name] = group[0][sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[col.name] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom atomAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.getAtom(1)\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || untypedScalarValue === undefined) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return atom === \"\" || atom === undefined\n else if (operator === \"notEmpty\") return !(atom === \"\" || atom === undefined)\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n return this.getAtomsFrom(1)\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType stringAtom\n cue compose\n example\n table\n compose My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n catchAllAtomType stringAtom\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`<a href=\"${value.includes(\"@\") ? \"mailto:\" : \"\"}${value}\">${name}</a>`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.getAtom(1)\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom atomAtom\n catchAllAtomType atomAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType anyAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nscrollFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType scriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType anyAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nscrollFillParser\n cue fill\n extends abstractColumnNameParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nquoteLineParser\n popularity 0.004172\n catchAllAtomType anyAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return localStorage.getItem(fileOrUrl) || \"\"\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = localStorage.getItem(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n localStorage.setItem(url, await blob.text())\n return localStorage.getItem(url)\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `<a href=\"${this.editUrl}\" class=\"abstractTextLinkParser\">Edit</a>`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"162.1.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particle[methodName])\n .map((particle, index) => particle[methodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"<body>\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `<!DOCTYPE html>\\n<html lang=\"${this.lang}\">\\n${bodyTag}${content}\\n</body>\\n</html>` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll() {\n await this.load()\n await this.buildOne()\n await this.buildTwo()\n }\n async buildOne() {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne()\n }\n }\n async buildTwo(externalFilesCopied = {}) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(externalFilesCopied)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concept[measure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) row[measureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else row[measureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCache[measureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCache[measureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCache[measureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|footer$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"footer\")) {\n const pushes = particle.getParticles(\"footer\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` <item>\n <title>${title}</title>\n <link>${canonicalUrl}</link>\n <pubDate>${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}</pubDate>\n </item>`\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType anyAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n description Table from inline delimited data.\n catchAllAtomType anyAtom\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
\ No newline at end of file
diff --git a/dist/libs.js b/dist/libs.js
index 20880d7..6e1b4ab 100644
--- a/dist/libs.js
+++ b/dist/libs.js
@@ -16227,7 +16227,7 @@ class Particle extends AbstractParticle {
})
return Object.keys(obj)
}
- getAncestorParticlesByInheritanceViaExtendsKeyword(key) {
+ getAncestorParticlesByInheritanceViaExtendsCue(key) {
const ancestorParticles = this._getAncestorParticles(
(particle, id) => particle._getParticlesByColumn(0, id),
particle => particle.get(key),
@@ -17726,7 +17726,7 @@ Particle.iris = `sepal_length,sepal_width,petal_length,petal_width,species
4.9,2.5,4.5,1.7,virginica
5.1,3.5,1.4,0.2,setosa
5,3.4,1.5,0.2,setosa`
-Particle.getVersion = () => "99.2.0"
+Particle.getVersion = () => "100.0.1"
class AbstractExtendibleParticle extends Particle {
_getFromExtended(cuePath) {
const hit = this._getParticleFromExtended(cuePath)
@@ -17835,7 +17835,7 @@ var ParsersConstantsMisc
var PreludeAtomTypeIds
;(function (PreludeAtomTypeIds) {
PreludeAtomTypeIds["anyAtom"] = "anyAtom"
- PreludeAtomTypeIds["keywordAtom"] = "keywordAtom"
+ PreludeAtomTypeIds["cueAtom"] = "cueAtom"
PreludeAtomTypeIds["extraAtomAtom"] = "extraAtomAtom"
PreludeAtomTypeIds["floatAtom"] = "floatAtom"
PreludeAtomTypeIds["numberAtom"] = "numberAtom"
@@ -18017,17 +18017,17 @@ class ParserBackedParticle extends Particle {
return BlobParser
}
_getAutocompleteResultsForCue(partialAtom) {
- const keywordMap = this.definition.cueMapWithDefinitions
- let keywords = Object.keys(keywordMap)
- if (partialAtom) keywords = keywords.filter(keyword => keyword.includes(partialAtom))
- return keywords
- .map(keyword => {
- const def = keywordMap[keyword]
+ const cueMap = this.definition.cueMapWithDefinitions
+ let cues = Object.keys(cueMap)
+ if (partialAtom) cues = cues.filter(cue => cue.includes(partialAtom))
+ return cues
+ .map(cue => {
+ const def = cueMap[cue]
if (def.suggestInAutocomplete === false) return false
const description = def.description
return {
- text: keyword,
- displayText: keyword + (description ? " " + description : "")
+ text: cue,
+ displayText: cue + (description ? " " + description : "")
}
})
.filter(i => i)
@@ -18681,7 +18681,7 @@ class ParsersAnyAtom extends AbstractParsersBackedAtom {
return this.getAtom()
}
}
-class ParsersKeywordAtom extends ParsersAnyAtom {
+class ParsersCueAtom extends ParsersAnyAtom {
_synthesizeAtom() {
return this._parserDefinitionParser.cueIfAny
}
@@ -18689,7 +18689,7 @@ class ParsersKeywordAtom extends ParsersAnyAtom {
return 1
}
}
-ParsersKeywordAtom.defaultPaint = "keyword"
+ParsersCueAtom.defaultPaint = "keyword"
class ParsersExtraAtomAtomTypeAtom extends AbstractParsersBackedAtom {
_isValid() {
return false
@@ -19255,6 +19255,10 @@ class ParsersParserConstantString extends AbstractParserConstantParser {
class ParsersParserConstantFloat extends AbstractParserConstantParser {}
class ParsersParserConstantBoolean extends AbstractParserConstantParser {}
class AbstractParserDefinitionParser extends AbstractExtendibleParticle {
+ constructor() {
+ super(...arguments)
+ this._isLooping = false
+ }
createParserCombinator() {
// todo: some of these should just be on nonRootParticles
const types = [
@@ -19615,9 +19619,19 @@ ${captures}
return this._cache_ancestorParserIdsArray
}
get programParserDefinitionCache() {
- if (!this._cache_parserDefinitionParsers) this._cache_parserDefinitionParsers = this.isRoot || this.hasParserDefinitions ? this.makeProgramParserDefinitionCache() : this.parent.programParserDefinitionCache
+ var _a
+ if (!this._cache_parserDefinitionParsers) {
+ if (this._isLooping) throw new Error(`Loop detected in ${this.id}`)
+ this._isLooping = true
+ this._cache_parserDefinitionParsers =
+ this.isRoot() || this.hasParserDefinitions
+ ? this.makeProgramParserDefinitionCache()
+ : ((_a = this.parent.programParserDefinitionCache[this.get(ParsersConstants.extends)]) === null || _a === void 0 ? void 0 : _a.programParserDefinitionCache) || this.parent.programParserDefinitionCache
+ this._isLooping = false
+ }
return this._cache_parserDefinitionParsers
}
+ get extendedDef() {}
get hasParserDefinitions() {
return !!this.getSubparticlesByParser(parserDefinitionParser).length
}
@@ -19640,7 +19654,7 @@ ${captures}
}
_toStumpString() {
const cue = this.cueIfAny
- const atomArray = this.atomParser.getAtomArray().filter((item, index) => index) // for now this only works for keyword langs
+ const atomArray = this.atomParser.getAtomArray().filter((item, index) => index) // for now this only works for cue langs
if (!atomArray.length)
// todo: remove this! just doing it for now until we refactor getAtomArray to handle catchAlls better.
return ""
@@ -20089,7 +20103,7 @@ HandParsersProgram._languages = {}
HandParsersProgram._parsers = {}
const PreludeKinds = {}
PreludeKinds[PreludeAtomTypeIds.anyAtom] = ParsersAnyAtom
-PreludeKinds[PreludeAtomTypeIds.keywordAtom] = ParsersKeywordAtom
+PreludeKinds[PreludeAtomTypeIds.cueAtom] = ParsersCueAtom
PreludeKinds[PreludeAtomTypeIds.floatAtom] = ParsersFloatAtom
PreludeKinds[PreludeAtomTypeIds.numberAtom] = ParsersFloatAtom
PreludeKinds[PreludeAtomTypeIds.bitAtom] = ParsersBitAtom
@@ -20110,7 +20124,7 @@ class UnknownParsersProgram extends Particle {
.setAtomsFrom(1, Array.from(new Set(rootParticleNames)))
return rootParticle
}
- _renameIntegerKeywords(clone) {
+ _renameIntegerCues(clone) {
// todo: why are we doing this?
for (let particle of clone.getTopDownArrayIterator()) {
const cueIsAnInteger = !!particle.cue.match(/^\d+$/)
@@ -20118,17 +20132,17 @@ class UnknownParsersProgram extends Particle {
if (cueIsAnInteger && parentCue) particle.setCue(HandParsersProgram.makeParserId(parentCue + UnknownParsersProgram._subparticleSuffix))
}
}
- _getKeywordMaps(clone) {
- const keywordsToChildKeywords = {}
- const keywordsToParticleInstances = {}
+ _getCueMaps(clone) {
+ const cuesToChildCues = {}
+ const cuesToParticleInstances = {}
for (let particle of clone.getTopDownArrayIterator()) {
const cue = particle.cue
- if (!keywordsToChildKeywords[cue]) keywordsToChildKeywords[cue] = {}
- if (!keywordsToParticleInstances[cue]) keywordsToParticleInstances[cue] = []
- keywordsToParticleInstances[cue].push(particle)
- particle.forEach(subparticle => (keywordsToChildKeywords[cue][subparticle.cue] = true))
+ if (!cuesToChildCues[cue]) cuesToChildCues[cue] = {}
+ if (!cuesToParticleInstances[cue]) cuesToParticleInstances[cue] = []
+ cuesToParticleInstances[cue].push(particle)
+ particle.forEach(subparticle => (cuesToChildCues[cue][subparticle.cue] = true))
}
- return { keywordsToChildKeywords: keywordsToChildKeywords, keywordsToParticleInstances: keywordsToParticleInstances }
+ return { cuesToChildCues, cuesToParticleInstances }
}
_inferParserDef(cue, globalAtomTypeMap, subparticleCues, instances) {
const edgeSymbol = this.edgeSymbol
@@ -20166,7 +20180,7 @@ class UnknownParsersProgram extends Particle {
if (needsCueProperty) particleDefParticle.set(ParsersConstants.cue, cue)
if (catchAllAtomType) particleDefParticle.set(ParsersConstants.catchAllAtomType, catchAllAtomType)
const atomLine = atomTypeIds.slice()
- atomLine.unshift(PreludeAtomTypeIds.keywordAtom)
+ atomLine.unshift(PreludeAtomTypeIds.cueAtom)
if (atomLine.length > 0) particleDefParticle.set(ParsersConstants.atoms, atomLine.join(edgeSymbol))
//if (!catchAllAtomType && atomTypeIds.length === 1) particleDefParticle.set(ParsersConstants.atoms, atomTypeIds[0])
// Todo: add conditional frequencies
@@ -20184,15 +20198,15 @@ class UnknownParsersProgram extends Particle {
// .setAtomsFrom(1, Array.from(new Set(rootParticleNames)))
// return rootParticle
// }
- inferParsersFileForAKeywordLanguage(parsersName) {
+ inferParsersFileForACueLanguage(parsersName) {
const clone = this.clone()
- this._renameIntegerKeywords(clone)
- const { keywordsToChildKeywords, keywordsToParticleInstances } = this._getKeywordMaps(clone)
+ this._renameIntegerCues(clone)
+ const { cuesToChildCues, cuesToParticleInstances } = this._getCueMaps(clone)
const globalAtomTypeMap = new Map()
- globalAtomTypeMap.set(PreludeAtomTypeIds.keywordAtom, undefined)
- const parserDefs = Object.keys(keywordsToChildKeywords)
+ globalAtomTypeMap.set(PreludeAtomTypeIds.cueAtom, undefined)
+ const parserDefs = Object.keys(cuesToChildCues)
.filter(identity => identity)
- .map(cue => this._inferParserDef(cue, globalAtomTypeMap, Object.keys(keywordsToChildKeywords[cue]), keywordsToParticleInstances[cue]))
+ .map(cue => this._inferParserDef(cue, globalAtomTypeMap, Object.keys(cuesToChildCues[cue]), cuesToParticleInstances[cue]))
const atomTypeDefs = []
globalAtomTypeMap.forEach((def, id) => atomTypeDefs.push(def ? def : id))
const particleBreakSymbol = this.particleBreakSymbol
@@ -20295,7 +20309,7 @@ atomPropertyNameAtom
paint variable.parameter
atomTypeIdAtom
- examples integerAtom keywordAtom someCustomAtom
+ examples integerAtom cueAtom someCustomAtom
extends javascriptSafeAlphaNumericIdentifierAtom
enumFromAtomTypes atomTypeIdAtom
paint storage
@@ -22301,7 +22315,7 @@ window.FusionFile = FusionFile
}
static cachedHandParsersProgramRoot = new HandParsersProgram(`// Atom parsers
anyAtom
-keywordAtom
+cueAtom
emptyAtom
extraAtom
paint invalid
@@ -22311,18 +22325,18 @@ attributeValueAtom
paint constant.language
componentTagNameAtom
paint variable.function
- extends keywordAtom
+ extends cueAtom
htmlTagNameAtom
paint variable.function
- extends keywordAtom
+ extends cueAtom
enum a abbr address area article aside b base bdi bdo blockquote body br button canvas caption code col colgroup datalist dd del details dfn dialog div dl dt em embed fieldset figure footer form h1 h2 h3 h4 h5 h6 head header hgroup hr html i iframe img input ins kbd keygen label legend li link main map mark menu menuitem meta meter nav noscript object ol optgroup option output p param pre progress q rb rp rt rtc ruby s samp script section select small source span strong styleTag sub summary sup table tbody td template textarea tfoot th thead time titleTag tr track u ul var video wbr
htmlAttributeNameAtom
paint entity.name.type
- extends keywordAtom
+ extends cueAtom
enum accept accept-charset accesskey action align alt async autocomplete autofocus autoplay bgcolor border charset checked class color cols colspan content contenteditable controls coords datetime default defer dir dirname disabled download draggable dropzone enctype for formaction headers height hidden high href hreflang http-equiv id ismap kind lang list loop low max maxlength media method min multiple muted name novalidate onabort onafterprint onbeforeprint onbeforeunload onblur oncanplay oncanplaythrough onchange onclick oncontextmenu oncopy oncuechange oncut ondblclick ondrag ondragend ondragenter ondragleave ondragover ondragstart ondrop ondurationchange onemptied onended onerror onfocus onhashchange oninput oninvalid onkeydown onkeypress onkeyup onload onloadeddata onloadedmetadata onloadstart onmousedown onmousemove onmouseout onmouseover onmouseup onmousewheel onoffline ononline onpagehide onpageshow onpaste onpause onplay onplaying onpopstate onprogress onratechange onreset onresize onscroll onsearch onseeked onseeking onselect onstalled onstorage onsubmit onsuspend ontimeupdate ontoggle onunload onvolumechange onwaiting onwheel open optimum pattern placeholder poster preload property readonly rel required reversed rows rowspan sandbox scope selected shape size sizes spellcheck src srcdoc srclang srcset start step style tabindex target title translate type usemap value width wrap
bernKeywordAtom
enum bern
- extends keywordAtom
+ extends cueAtom
// Line parsers
stumpParser
@@ -23145,9 +23159,9 @@ bernParser
}
static cachedHandParsersProgramRoot = new HandParsersProgram(`// Atom Parsers
anyAtom
-keywordAtom
+cueAtom
commentKeywordAtom
- extends keywordAtom
+ extends cueAtom
paint comment
enum comment
extraAtom
@@ -23161,10 +23175,9 @@ selectorAtom
vendorPrefixCueAtom
description Properties like -moz-column-fill
paint variable.function
- extends keywordAtom
-cueAtom
+ extends cueAtom
+propertyNameAtom
paint variable.function
- extends keywordAtom
// todo Where are these coming from? Can we add a url link
enum align-content align-items align-self all animation animation-delay animation-direction animation-duration animation-fill-mode animation-iteration-count animation-name animation-play-state animation-timing-function backface-visibility background background-attachment background-blend-mode background-clip background-color background-image background-origin background-position background-repeat background-size border border-bottom border-bottom-color border-bottom-left-radius border-bottom-right-radius border-bottom-style border-bottom-width border-collapse border-color border-image border-image-outset border-image-repeat border-image-slice border-image-source border-image-width border-left border-left-color border-left-style border-left-width border-radius border-right border-right-color border-right-style border-right-width border-spacing border-style border-top border-top-color border-top-left-radius border-top-right-radius border-top-style border-top-width border-width bottom box-shadow box-sizing break-inside caption-side clear clip color column-count column-fill column-gap column-rule column-rule-color column-rule-style column-rule-width column-span column-width columns content counter-increment counter-reset cursor direction display empty-atoms fill filter flex flex-basis flex-direction flex-flow flex-grow flex-shrink flex-wrap float font @font-face font-family font-size font-size-adjust font-stretch font-style font-variant font-weight hanging-punctuation height hyphens justify-content @keyframes left letter-spacing line-height list-style list-style-image list-style-position list-style-type margin margin-bottom margin-left margin-right margin-top max-height max-width @media min-height min-width nav-down nav-index nav-left nav-right nav-up opacity order outline outline-color outline-offset outline-style outline-width overflow overflow-x overflow-y padding padding-bottom padding-left padding-right padding-top page-break-after page-break-before page-break-inside perspective perspective-origin position quotes resize right tab-size table-layout text-align text-align-last text-decoration text-decoration-color text-decoration-line text-decoration-style text-indent text-justify text-overflow text-shadow text-transform top transform transform-origin transform-style transition transition-delay transition-duration transition-property transition-timing-function unicode-bidi vertical-align visibility white-space width atom-break atom-spacing atom-wrap z-index overscroll-behavior-x user-select -ms-touch-action -webkit-user-select -webkit-touch-callout -moz-user-select touch-action -ms-user-select -khtml-user-select gap grid-auto-flow grid-column grid-column-end grid-column-gap grid-column-start grid-gap grid-row grid-row-end grid-row-gap grid-row-start grid-template-columns grid-template-rows justify-items justify-self
errorAtom
@@ -23205,7 +23218,7 @@ propertyParser
compile(spaces) {
return \`\${spaces}\${this.cue}: \${this.content};\`
}
- atoms cueAtom
+ atoms propertyNameAtom
variableParser
extends propertyParser
pattern --
@@ -23255,7 +23268,7 @@ selectorParser
createParserCombinator() {
return new Particle.ParserCombinator(errorParser, undefined, undefined)
}
- get cueAtom() {
+ get propertyNameAtom() {
return this.getAtom(0)
}
get cssValueAtom() {
diff --git a/package.json b/package.json
index 5612179..c8a7167 100644
--- a/package.json
+++ b/package.json
@@ -9,8 +9,8 @@
},
"dependencies": {
"jquery": "^3.6.0",
- "scroll-cli": "^162.0.1",
- "scrollsdk": "^99.2.0"
+ "scroll-cli": "^162.1.0",
+ "scrollsdk": "^100.0.1"
},
"devDependencies": {
"tap": "^18.7.2"
diff --git a/scroll.parsers b/scroll.parsers
index 578e78a..90d8f85 100644
--- a/scroll.parsers
+++ b/scroll.parsers
@@ -93,7 +93,7 @@ atomParserAtom
atomPropertyNameAtom
paint variable.parameter
atomTypeIdAtom
- examples integerAtom keywordAtom someCustomAtom
+ examples integerAtom cueAtom someCustomAtom
extends javascriptSafeAlphaNumericIdentifierAtom
enumFromAtomTypes atomTypeIdAtom
paint storage
@@ -732,10 +732,10 @@ scrollVideoParser
description Play video files.
widthParser
cueFromId
- atoms cueAtom
+ atoms cueAtom integerAtom
heightParser
cueFromId
- atoms cueAtom
+ atoms cueAtom integerAtom
javascript
tag = "video"
quickVideoParser
@@ -744,13 +744,6 @@ quickVideoParser
atoms urlAtom
pattern ^[^\s]+\.(mp4|webm|avi|mov)
int atomIndex 0
- widthParser
- // todo: fix inheritance bug
- cueFromId
- atoms cueAtom
- heightParser
- cueFromId
- atoms cueAtom
quickParagraphParser
popularity 0.001881
cue *
@@ -5647,7 +5640,7 @@ scrollParser
}
get scrollVersion() {
// currently manually updated
- return "162.0.0"
+ return "162.1.0"
}
// Use the first paragraph for the description
// todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)
------------------------------------------------------------
commit 35fcd5eaa9ef016113ebf6abd3abdd497bd84088
Author: Breck Yunits <breck7@gmail.com>
Date: Fri Dec 6 10:33:59 2024 -1000
diff --git a/package.json b/package.json
index e27a036..5612179 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
},
"dependencies": {
"jquery": "^3.6.0",
- "scroll-cli": "file:../scroll",
+ "scroll-cli": "^162.0.1",
"scrollsdk": "^99.2.0"
},
"devDependencies": {
------------------------------------------------------------
commit 3b30f9ca5a6180144d4103e9d3bf570697cc3884
Author: Breck Yunits <breck7@gmail.com>
Date: Fri Dec 6 10:32:26 2024 -1000
diff --git a/d3.js b/.d3.js
similarity index 100%
rename from d3.js
rename to .d3.js
diff --git a/dark.css b/.dark.css
similarity index 100%
rename from dark.css
rename to .dark.css
diff --git a/datatables.css b/.datatables.css
similarity index 100%
rename from datatables.css
rename to .datatables.css
diff --git a/datatables.js b/.datatables.js
similarity index 100%
rename from datatables.js
rename to .datatables.js
diff --git a/dayjs.min.js b/.dayjs.min.js
similarity index 100%
rename from dayjs.min.js
rename to .dayjs.min.js
diff --git a/gazette.css b/.gazette.css
similarity index 100%
rename from gazette.css
rename to .gazette.css
diff --git a/helpfulNotFound.js b/.helpfulNotFound.js
similarity index 100%
rename from helpfulNotFound.js
rename to .helpfulNotFound.js
diff --git a/inspector.css b/.inspector.css
similarity index 100%
rename from inspector.css
rename to .inspector.css
diff --git a/jquery-3.7.1.min.js b/.jquery-3.7.1.min.js
similarity index 100%
rename from jquery-3.7.1.min.js
rename to .jquery-3.7.1.min.js
diff --git a/katex.min.css b/.katex.min.css
similarity index 100%
rename from katex.min.css
rename to .katex.min.css
diff --git a/katex.min.js b/.katex.min.js
similarity index 100%
rename from katex.min.js
rename to .katex.min.js
diff --git a/leaflet.css b/.leaflet.css
similarity index 100%
rename from leaflet.css
rename to .leaflet.css
diff --git a/leaflet.js b/.leaflet.js
similarity index 100%
rename from leaflet.js
rename to .leaflet.js
diff --git a/plot.js b/.plot.js
similarity index 100%
rename from plot.js
rename to .plot.js
diff --git a/prestige.css b/.prestige.css
similarity index 100%
rename from prestige.css
rename to .prestige.css
diff --git a/roboto.css b/.roboto.css
similarity index 100%
rename from roboto.css
rename to .roboto.css
diff --git a/scrollLibs.js b/.scrollLibs.js
similarity index 100%
rename from scrollLibs.js
rename to .scrollLibs.js
diff --git a/slideshow.js b/.slideshow.js
similarity index 100%
rename from slideshow.js
rename to .slideshow.js
diff --git a/sparkline.js b/.sparkline.js
similarity index 100%
rename from sparkline.js
rename to .sparkline.js
diff --git a/tableSearch.js b/.tableSearch.js
similarity index 100%
rename from tableSearch.js
rename to .tableSearch.js
diff --git a/tufte.css b/.tufte.css
similarity index 100%
rename from tufte.css
rename to .tufte.css
diff --git a/build.js b/build.js
index 262f844..fd04f66 100755
--- a/build.js
+++ b/build.js
@@ -11,7 +11,7 @@ const { DefaultScrollParser } = require("scroll-cli")
const libPaths = `../sdk/particleComponentFramework/sweepercraft/lib/mousetrap.min.js
node_modules/jquery/dist/jquery.min.js
lib/jquery-ui.min.js
-node_modules/scroll-cli/external/dayjs.min.js
+node_modules/scroll-cli/external/.dayjs.min.js
lib/jquery.ui.touch-punch.min.js
../sdk/sandbox/lib/codemirror.js
../sdk/sandbox/lib/show-hint.js
diff --git a/datatables.min.js b/datatables.min.js
deleted file mode 100644
index 4daacce..0000000
--- a/datatables.min.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * This combined file was created by the DataTables downloader builder:
- * https://datatables.net/download
- *
- * To rebuild or modify this file with the latest versions of the included
- * software please visit:
- * https://datatables.net/download/#dt/dt-2.1.3/b-3.1.1/b-html5-3.1.1
- *
- * Included libraries:
- * DataTables 2.1.3, Buttons 3.1.1, HTML5 export 3.1.1
- */
-
-/*! DataTables 2.1.3
- * © SpryMedia Ltd - datatables.net/license
- */
-!function(n){"use strict";var a;"function"==typeof define&&define.amd?define(["jquery"],function(e){return n(e,window,document)}):"object"==typeof exports?(a=require("jquery"),"undefined"==typeof window?module.exports=function(e,t){return e=e||window,t=t||a(e),n(t,e,e.document)}:module.exports=n(a,window,window.document)):window.DataTable=n(jQuery,window,document)}(function(H,W,_){"use strict";function f(e){var t=parseInt(e,10);return!isNaN(t)&&isFinite(e)?t:null}function s(e,t,n,a){var r=typeof e,o="string"==r;return"number"==r||"bigint"==r||!(!a||!T(e))||(t&&o&&(e=E(e,t)),n&&o&&(e=e.replace(P,"")),!isNaN(parseFloat(e))&&isFinite(e))}function c(e,t,n,a){var r;return!(!a||!T(e))||("string"!=typeof e||!e.match(/<(input|select)/i))&&(T(r=e)||"string"==typeof r)&&!!s(L(e),t,n,a)||null}function b(e,t,n,a){var r=[],o=0,i=t.length;if(void 0!==a)for(;o<i;o++)e[t[o]][n]&&r.push(e[t[o]][n][a]);else for(;o<i;o++)e[t[o]]&&r.push(e[t[o]][n]);return r}function h(e,t){var n,a=[];void 0===t?(t=0,n=e):(n=t,t=e);for(var r=t;r<n;r++)a.push(r);return a}function A(e){for(var t=[],n=0,a=e.length;n<a;n++)e[n]&&t.push(e[n]);return t}var C,X,t,e,V=function(e,P){var E,k,M;return V.factory(e,P)?V:this instanceof V?H(e).DataTable(P):(k=void 0===(P=e),M=(E=this).length,k&&(P={}),this.api=function(){return new X(this)},this.each(function(){var e=1<M?et({},P,!0):P,t=0,n=this.getAttribute("id"),a=V.defaults,r=H(this);if("table"!=this.nodeName.toLowerCase())$(null,0,"Non-table node initialisation ("+this.nodeName+")",2);else{H(this).trigger("options.dt",e),Q(a),K(a.column),q(a,a,!0),q(a.column,a.column,!0),q(a,H.extend(e,r.data()),!0);var o=V.settings;for(t=0,R=o.length;t<R;t++){var i=o[t];if(i.nTable==this||i.nTHead&&i.nTHead.parentNode==this||i.nTFoot&&i.nTFoot.parentNode==this){var l=(void 0!==e.bRetrieve?e:a).bRetrieve,s=(void 0!==e.bDestroy?e:a).bDestroy;if(k||l)return i.oInstance;if(s){new V.Api(i).destroy();break}return void $(i,0,"Cannot reinitialise DataTable",3)}if(i.sTableId==this.id){o.splice(t,1);break}}null!==n&&""!==n||(n="DataTables_Table_"+V.ext._unique++,this.id=n);var u,c=H.extend(!0,{},V.models.oSettings,{sDestroyWidth:r[0].style.width,sInstance:n,sTableId:n,colgroup:H("<colgroup>").prependTo(this),fastData:function(e,t,n){return B(c,e,t,n)}}),n=(c.nTable=this,c.oInit=e,o.push(c),c.api=new X(c),c.oInstance=1===E.length?E:r.dataTable(),Q(e),e.aLengthMenu&&!e.iDisplayLength&&(e.iDisplayLength=Array.isArray(e.aLengthMenu[0])?e.aLengthMenu[0][0]:H.isPlainObject(e.aLengthMenu[0])?e.aLengthMenu[0].value:e.aLengthMenu[0]),e=et(H.extend(!0,{},a),e),z(c.oFeatures,e,["bPaginate","bLengthChange","bFilter","bSort","bSortMulti","bInfo","bProcessing","bAutoWidth","bSortClasses","bServerSide","bDeferRender"]),z(c,e,["ajax","fnFormatNumber","sServerMethod","aaSorting","aaSortingFixed","aLengthMenu","sPaginationType","iStateDuration","bSortCellsTop","iTabIndex","sDom","fnStateLoadCallback","fnStateSaveCallback","renderer","searchDelay","rowId","caption","layout","orderDescReverse",["iCookieDuration","iStateDuration"],["oSearch","oPreviousSearch"],["aoSearchCols","aoPreSearchCols"],["iDisplayLength","_iDisplayLength"]]),z(c.oScroll,e,[["sScrollX","sX"],["sScrollXInner","sXInner"],["sScrollY","sY"],["bScrollCollapse","bCollapse"]]),z(c.oLanguage,e,"fnInfoCallback"),Y(c,"aoDrawCallback",e.fnDrawCallback),Y(c,"aoStateSaveParams",e.fnStateSaveParams),Y(c,"aoStateLoadParams",e.fnStateLoadParams),Y(c,"aoStateLoaded",e.fnStateLoaded),Y(c,"aoRowCallback",e.fnRowCallback),Y(c,"aoRowCreatedCallback",e.fnCreatedRow),Y(c,"aoHeaderCallback",e.fnHeaderCallback),Y(c,"aoFooterCallback",e.fnFooterCallback),Y(c,"aoInitComplete",e.fnInitComplete),Y(c,"aoPreDrawCallback",e.fnPreDrawCallback),c.rowIdFn=U(e.rowId),c),d=(V.__browser||(f={},V.__browser=f,p=H("<div/>").css({position:"fixed",top:0,left:-1*W.pageXOffset,height:1,width:1,overflow:"hidden"}).append(H("<div/>").css({position:"absolute",top:1,left:1,width:100,overflow:"scroll"}).append(H("<div/>").css({width:"100%",height:10}))).appendTo("body"),d=p.children(),u=d.children(),f.barWidth=d[0].offsetWidth-d[0].clientWidth,f.bScrollbarLeft=1!==Math.round(u.offset().left),p.remove()),H.extend(n.oBrowser,V.__browser),n.oScroll.iBarWidth=V.__browser.barWidth,c.oClasses),f=(H.extend(d,V.ext.classes,e.oClasses),r.addClass(d.table),c.oFeatures.bPaginate||(e.iDisplayStart=0),void 0===c.iInitDisplayStart&&(c.iInitDisplayStart=e.iDisplayStart,c._iDisplayStart=e.iDisplayStart),e.iDeferLoading),h=(null!==f&&(c.deferLoading=!0,u=Array.isArray(f),c._iRecordsDisplay=u?f[0]:f,c._iRecordsTotal=u?f[1]:f),[]),p=this.getElementsByTagName("thead"),n=Ae(c,p[0]);if(e.aoColumns)h=e.aoColumns;else if(n.length)for(R=n[t=0].length;t<R;t++)h.push(null);for(t=0,R=h.length;t<R;t++)ee(c);var g,v,m,b,y,D,x,S=c,w=e.aoColumnDefs,T=h,_=n,C=function(e,t){te(c,e,t)},L=S.aoColumns;if(T)for(g=0,v=T.length;g<v;g++)T[g]&&T[g].name&&(L[g].sName=T[g].name);if(w)for(g=w.length-1;0<=g;g--){var I=void 0!==(x=w[g]).target?x.target:void 0!==x.targets?x.targets:x.aTargets;for(Array.isArray(I)||(I=[I]),m=0,b=I.length;m<b;m++){var A=I[m];if("number"==typeof A&&0<=A){for(;L.length<=A;)ee(S);C(A,x)}else if("number"==typeof A&&A<0)C(L.length+A,x);else if("string"==typeof A)for(y=0,D=L.length;y<D;y++)"_all"===A?C(y,x):-1!==A.indexOf(":name")?L[y].sName===A.replace(":name","")&&C(y,x):_.forEach(function(e){e[y]&&(e=H(e[y].cell),A.match(/^[a-z][\w-]*$/i)&&(A="."+A),e.is(A))&&C(y,x)})}}if(T)for(g=0,v=T.length;g<v;g++)C(g,T[g]);var F,n=r.children("tbody").find("tr").eq(0),N=(n.length&&(F=function(e,t){return null!==e.getAttribute("data-"+t)?t:null},H(n[0]).children("th, td").each(function(e,t){var n,a=c.aoColumns[e];a||$(c,0,"Incorrect column count",18),a.mData===e&&(n=F(t,"sort")||F(t,"order"),t=F(t,"filter")||F(t,"search"),null===n&&null===t||(a.mData={_:e+".display",sort:null!==n?e+".@data-"+n:void 0,type:null!==n?e+".@data-"+n:void 0,filter:null!==t?e+".@data-"+t:void 0},a._isArrayHost=!0,te(c,e)))})),Y(c,"aoDrawCallback",Qe),c.oFeatures);if(e.bStateSave&&(N.bStateSave=!0),void 0===e.aaSorting)for(var j=c.aaSorting,t=0,R=j.length;t<R;t++)j[t][1]=c.aoColumns[t].asSorting[0];Ze(c),Y(c,"aoDrawCallback",function(){(c.bSorted||"ssp"===J(c)||N.bDeferRender)&&Ze(c)});var n=r.children("caption"),n=(c.caption&&(n=0===n.length?H("<caption/>").appendTo(r):n).html(c.caption),n.length&&(n[0]._captionSide=n.css("caption-side"),c.captionNode=n[0]),0===p.length&&(p=H("<thead/>").appendTo(r)),c.nTHead=p[0],H("tr",p).addClass(d.thead.row),r.children("tbody")),n=(0===n.length&&(n=H("<tbody/>").insertAfter(p)),c.nTBody=n[0],r.children("tfoot")),O=(0===n.length&&(n=H("<tfoot/>").appendTo(r)),c.nTFoot=n[0],H("tr",n).addClass(d.tfoot.row),c.aiDisplay=c.aiDisplayMaster.slice(),c.bInitialised=!0,c.oLanguage);H.extend(!0,O,e.oLanguage),O.sUrl?H.ajax({dataType:"json",url:O.sUrl,success:function(e){q(a.oLanguage,e),H.extend(!0,O,e,c.oInit.oLanguage),G(c,null,"i18n",[c],!0),Me(c)},error:function(){$(c,0,"i18n file loading error",21),Me(c)}}):(G(c,null,"i18n",[c]),Me(c))}}),E=null,this)},g=(V.ext=C={buttons:{},classes:{},builder:"dt/dt-2.1.3/b-3.1.1/b-html5-3.1.1",errMode:"alert",feature:[],features:{},search:[],selector:{cell:[],column:[],row:[]},legacy:{ajax:null},pager:{},renderer:{pageButton:{},header:{}},order:{},type:{className:{},detect:[],render:{},search:{},order:{}},_unique:0,fnVersionCheck:V.fnVersionCheck,iApiIndex:0,sVersion:V.version},H.extend(C,{afnFiltering:C.search,aTypes:C.type.detect,ofnSearch:C.type.search,oSort:C.type.order,afnSortData:C.order,aoFeatures:C.feature,oStdClasses:C.classes,oPagination:C.pager}),H.extend(V.ext.classes,{container:"dt-container",empty:{row:"dt-empty"},info:{container:"dt-info"},layout:{row:"dt-layout-row",cell:"dt-layout-cell",tableRow:"dt-layout-table",tableCell:"",start:"dt-layout-start",end:"dt-layout-end",full:"dt-layout-full"},length:{container:"dt-length",select:"dt-input"},order:{canAsc:"dt-orderable-asc",canDesc:"dt-orderable-desc",isAsc:"dt-ordering-asc",isDesc:"dt-ordering-desc",none:"dt-orderable-none",position:"sorting_"},processing:{container:"dt-processing"},scrolling:{body:"dt-scroll-body",container:"dt-scroll",footer:{self:"dt-scroll-foot",inner:"dt-scroll-footInner"},header:{self:"dt-scroll-head",inner:"dt-scroll-headInner"}},search:{container:"dt-search",input:"dt-input"},table:"dataTable",tbody:{cell:"",row:""},thead:{cell:"",row:""},tfoot:{cell:"",row:""},paging:{active:"current",button:"dt-paging-button",container:"dt-paging",disabled:"disabled"}}),{}),F=/[\r\n\u2028]/g,N=/<([^>]*>)/g,j=Math.pow(2,28),R=/^\d{2,4}[./-]\d{1,2}[./-]\d{1,2}([T ]{1}\d{1,2}[:.]\d{2}([.:]\d{2})?)?$/,O=new RegExp("(\\"+["/",".","*","+","?","|","(",")","[","]","{","}","\\","$","^","-"].join("|\\")+")","g"),P=/['\u00A0,$£€¥%\u2009\u202F\u20BD\u20a9\u20BArfkɃΞ]/gi,T=function(e){return!e||!0===e||"-"===e},E=function(e,t){return g[t]||(g[t]=new RegExp(Pe(t),"g")),"string"==typeof e&&"."!==t?e.replace(/\./g,"").replace(g[t],"."):e},m=function(e,t,n){var a=[],r=0,o=e.length;if(void 0!==n)for(;r<o;r++)e[r]&&e[r][t]&&a.push(e[r][t][n]);else for(;r<o;r++)e[r]&&a.push(e[r][t]);return a},L=function(e){if(!e||"string"!=typeof e)return e;if(e.length>j)throw new Error("Exceeded max str len");var t;for(e=e.replace(N,"");(e=(t=e).replace(/<script/i,""))!==t;);return t},u=function(e){return"string"==typeof(e=Array.isArray(e)?e.join(","):e)?e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""):e},k=function(e,t){var n;return"string"!=typeof e?e:(n=e.normalize("NFD")).length!==e.length?(!0===t?e+" ":"")+n.replace(/[\u0300-\u036f]/g,""):n},x=function(e){if(Array.from&&Set)return Array.from(new Set(e));if(function(e){if(!(e.length<2))for(var t=e.slice().sort(),n=t[0],a=1,r=t.length;a<r;a++){if(t[a]===n)return!1;n=t[a]}return!0}(e))return e.slice();var t,n,a,r=[],o=e.length,i=0;e:for(n=0;n<o;n++){for(t=e[n],a=0;a<i;a++)if(r[a]===t)continue e;r.push(t),i++}return r},M=function(e,t){if(Array.isArray(t))for(var n=0;n<t.length;n++)M(e,t[n]);else e.push(t);return e};function y(t,e){e&&e.split(" ").forEach(function(e){e&&t.classList.add(e)})}function Z(t){var n,a,r={};H.each(t,function(e){(n=e.match(/^([^A-Z]+?)([A-Z])/))&&-1!=="a aa ai ao as b fn i m o s ".indexOf(n[1]+" ")&&(a=e.replace(n[0],n[2].toLowerCase()),r[a]=e,"o"===n[1])&&Z(t[e])}),t._hungarianMap=r}function q(t,n,a){var r;t._hungarianMap||Z(t),H.each(n,function(e){void 0===(r=t._hungarianMap[e])||!a&&void 0!==n[r]||("o"===r.charAt(0)?(n[r]||(n[r]={}),H.extend(!0,n[r],n[e]),q(t[r],n[r],a)):n[r]=n[e])})}V.util={diacritics:function(e,t){if("function"!=typeof e)return k(e,t);k=e},debounce:function(n,a){var r;return function(){var e=this,t=arguments;clearTimeout(r),r=setTimeout(function(){n.apply(e,t)},a||250)}},throttle:function(a,e){var r,o,i=void 0!==e?e:200;return function(){var e=this,t=+new Date,n=arguments;r&&t<r+i?(clearTimeout(o),o=setTimeout(function(){r=void 0,a.apply(e,n)},i)):(r=t,a.apply(e,n))}},escapeRegex:function(e){return e.replace(O,"\\$1")},set:function(a){var f;return H.isPlainObject(a)?V.util.set(a._):null===a?function(){}:"function"==typeof a?function(e,t,n){a(e,"set",t,n)}:"string"!=typeof a||-1===a.indexOf(".")&&-1===a.indexOf("[")&&-1===a.indexOf("(")?function(e,t){e[a]=t}:(f=function(e,t,n){for(var a,r,o,i,l=ge(n),n=l[l.length-1],s=0,u=l.length-1;s<u;s++){if("__proto__"===l[s]||"constructor"===l[s])throw new Error("Cannot set prototype values");if(a=l[s].match(pe),r=l[s].match(p),a){if(l[s]=l[s].replace(pe,""),e[l[s]]=[],(a=l.slice()).splice(0,s+1),i=a.join("."),Array.isArray(t))for(var c=0,d=t.length;c<d;c++)f(o={},t[c],i),e[l[s]].push(o);else e[l[s]]=t;return}r&&(l[s]=l[s].replace(p,""),e=e[l[s]](t)),null!==e[l[s]]&&void 0!==e[l[s]]||(e[l[s]]={}),e=e[l[s]]}n.match(p)?e[n.replace(p,"")](t):e[n.replace(pe,"")]=t},function(e,t){return f(e,t,a)})},get:function(r){var o,f;return H.isPlainObject(r)?(o={},H.each(r,function(e,t){t&&(o[e]=V.util.get(t))}),function(e,t,n,a){var r=o[t]||o._;return void 0!==r?r(e,t,n,a):e}):null===r?function(e){return e}:"function"==typeof r?function(e,t,n,a){return r(e,t,n,a)}:"string"!=typeof r||-1===r.indexOf(".")&&-1===r.indexOf("[")&&-1===r.indexOf("(")?function(e){return e[r]}:(f=function(e,t,n){var a,r,o;if(""!==n)for(var i=ge(n),l=0,s=i.length;l<s;l++){if(d=i[l].match(pe),a=i[l].match(p),d){if(i[l]=i[l].replace(pe,""),""!==i[l]&&(e=e[i[l]]),r=[],i.splice(0,l+1),o=i.join("."),Array.isArray(e))for(var u=0,c=e.length;u<c;u++)r.push(f(e[u],t,o));var d=d[0].substring(1,d[0].length-1);e=""===d?r:r.join(d);break}if(a)i[l]=i[l].replace(p,""),e=e[i[l]]();else{if(null===e||null===e[i[l]])return null;if(void 0===e||void 0===e[i[l]])return;e=e[i[l]]}}return e},function(e,t){return f(e,t,r)})},stripHtml:function(e){var t=typeof e;if("function"!=t)return"string"==t?L(e):e;L=e},escapeHtml:function(e){var t=typeof e;if("function"!=t)return"string"==t||Array.isArray(e)?u(e):e;u=e},unique:x};var r=function(e,t,n){void 0!==e[t]&&(e[n]=e[t])};function Q(e){r(e,"ordering","bSort"),r(e,"orderMulti","bSortMulti"),r(e,"orderClasses","bSortClasses"),r(e,"orderCellsTop","bSortCellsTop"),r(e,"order","aaSorting"),r(e,"orderFixed","aaSortingFixed"),r(e,"paging","bPaginate"),r(e,"pagingType","sPaginationType"),r(e,"pageLength","iDisplayLength"),r(e,"searching","bFilter"),"boolean"==typeof e.sScrollX&&(e.sScrollX=e.sScrollX?"100%":""),"boolean"==typeof e.scrollX&&(e.scrollX=e.scrollX?"100%":"");var t=e.aoSearchCols;if(t)for(var n=0,a=t.length;n<a;n++)t[n]&&q(V.models.oSearch,t[n]);e.serverSide&&!e.searchDelay&&(e.searchDelay=400)}function K(e){r(e,"orderable","bSortable"),r(e,"orderData","aDataSort"),r(e,"orderSequence","asSorting"),r(e,"orderDataType","sortDataType");var t=e.aDataSort;"number"!=typeof t||Array.isArray(t)||(e.aDataSort=[t])}function ee(e){var t=V.defaults.column,n=e.aoColumns.length,t=H.extend({},V.models.oColumn,t,{aDataSort:t.aDataSort||[n],mData:t.mData||n,idx:n,searchFixed:{},colEl:H("<col>").attr("data-dt-column",n)}),t=(e.aoColumns.push(t),e.aoPreSearchCols);t[n]=H.extend({},V.models.oSearch,t[n])}function te(e,t,n){function a(e){return"string"==typeof e&&-1!==e.indexOf("@")}var r=e.aoColumns[t],o=(null!=n&&(K(n),q(V.defaults.column,n,!0),void 0===n.mDataProp||n.mData||(n.mData=n.mDataProp),n.sType&&(r._sManualType=n.sType),n.className&&!n.sClass&&(n.sClass=n.className),t=r.sClass,H.extend(r,n),z(r,n,"sWidth","sWidthOrig"),t!==r.sClass&&(r.sClass=t+" "+r.sClass),void 0!==n.iDataSort&&(r.aDataSort=[n.iDataSort]),z(r,n,"aDataSort")),r.mData),i=U(o);r.mRender&&Array.isArray(r.mRender)&&(n=(t=r.mRender.slice()).shift(),r.mRender=V.render[n].apply(W,t)),r._render=r.mRender?U(r.mRender):null;r._bAttrSrc=H.isPlainObject(o)&&(a(o.sort)||a(o.type)||a(o.filter)),r._setter=null,r.fnGetData=function(e,t,n){var a=i(e,t,void 0,n);return r._render&&t?r._render(a,t,e,n):a},r.fnSetData=function(e,t,n){return v(o)(e,t,n)},"number"==typeof o||r._isArrayHost||(e._rowReadObject=!0),e.oFeatures.bSort||(r.bSortable=!1)}function ne(e){var t=e;if(t.oFeatures.bAutoWidth){var n,a,r=t.nTable,o=t.aoColumns,i=t.oScroll,l=i.sY,s=i.sX,i=i.sXInner,u=ie(t,"bVisible"),c=r.getAttribute("width"),d=r.parentNode,f=r.style.width,f=(f||c||(r.style.width="100%",f="100%"),f&&-1!==f.indexOf("%")&&(c=f),G(t,null,"column-calc",{visible:u},!1),H(r.cloneNode()).css("visibility","hidden").removeAttr("id")),h=(f.append("<tbody>"),H("<tr/>").appendTo(f.find("tbody")));for(f.append(H(t.nTHead).clone()).append(H(t.nTFoot).clone()),f.find("tfoot th, tfoot td").css("width",""),f.find("thead th, thead td").each(function(){var e=ce(t,this,!0,!1);e?(this.style.width=e,s&&H(this).append(H("<div/>").css({width:e,margin:0,padding:0,border:0,height:1}))):this.style.width=""}),n=0;n<u.length;n++){p=u[n],a=o[p];var p=function(e,t){var n=e.aoColumns[t];if(!n.maxLenString){for(var a,r="",o=-1,i=0,l=e.aiDisplayMaster.length;i<l;i++){var s=e.aiDisplayMaster[i],s=De(e,s)[t],s=s&&"object"==typeof s&&s.nodeType?s.innerHTML:s+"";s=s.replace(/id=".*?"/g,"").replace(/name=".*?"/g,""),(a=L(s).replace(/ /g," ")).length>o&&(r=s,o=a.length)}n.maxLenString=r}return n.maxLenString}(t,p),g=C.type.className[a.sType],v=p+a.sContentPadding,p=-1===p.indexOf("<")?_.createTextNode(v):v;H("<td/>").addClass(g).addClass(a.sClass).append(p).appendTo(h)}H("[name]",f).removeAttr("name");var m=H("<div/>").css(s||l?{position:"absolute",top:0,left:0,height:1,right:0,overflow:"hidden"}:{}).append(f).appendTo(d),b=(s&&i?f.width(i):s?(f.css("width","auto"),f.removeAttr("width"),f.width()<d.clientWidth&&c&&f.width(d.clientWidth)):l?f.width(d.clientWidth):c&&f.width(c),0),y=f.find("tbody tr").eq(0).children();for(n=0;n<u.length;n++){var D=y[n].getBoundingClientRect().width;b+=D,o[u[n]].sWidth=I(D)}r.style.width=I(b),m.remove(),c&&(r.style.width=I(c)),!c&&!s||t._reszEvt||(H(W).on("resize.DT-"+t.sInstance,V.util.throttle(function(){t.bDestroying||ne(t)})),t._reszEvt=!0)}for(var x=e,S=x.aoColumns,w=0;w<S.length;w++){var T=ce(x,[w],!1,!1);S[w].colEl.css("width",T)}i=e.oScroll;""===i.sY&&""===i.sX||Be(e),G(e,null,"column-sizing",[e])}function ae(e,t){e=ie(e,"bVisible");return"number"==typeof e[t]?e[t]:null}function re(e,t){e=ie(e,"bVisible").indexOf(t);return-1!==e?e:null}function oe(e){var t=e.aoHeader,n=e.aoColumns,a=0;if(t.length)for(var r=0,o=t[0].length;r<o;r++)n[r].bVisible&&"none"!==H(t[0][r].cell).css("display")&&a++;return a}function ie(e,n){var a=[];return e.aoColumns.map(function(e,t){e[n]&&a.push(t)}),a}function le(e,t){return!0===t?e.name:t}function se(e){var t,n,a,r,o,i,l,s,u=e.aoColumns,c=e.aoData,d=V.ext.type.detect;if("ssp"!==J(e))for(t=0,n=u.length;t<n;t++){if(s=[],!(l=u[t]).sType&&l._sManualType)l.sType=l._sManualType;else if(!l.sType){for(a=0,r=d.length;a<r;a++){var f=d[a],h=f.oneOf,p=f.allOf||f,g=f.init,v=!1,m=null;if(g&&(m=le(f,g(e,l,t)))){l.sType=m;break}for(o=0,i=c.length;o<i;o++)if(c[o]){if(void 0===s[o]&&(s[o]=B(e,o,t,"type")),h&&!v&&(v=le(f,h(s[o],e))),!(m=le(f,p(s[o],e)))&&a!==d.length-3)break;if("html"===m&&!T(s[o]))break}if(h&&v&&m||!h&&m){l.sType=m;break}}l.sType||(l.sType="string")}var b=C.type.className[l.sType],b=(b&&(ue(e.aoHeader,t,b),ue(e.aoFooter,t,b)),C.type.render[l.sType]);if(b&&!l._render){l._render=V.util.get(b),y=w=S=x=D=void 0;for(var y,D=e,x=t,S=D.aoData,w=0;w<S.length;w++)S[w].nTr&&(y=B(D,w,x,"display"),S[w].displayData[x]=y,he(S[w].anCells[x],y))}}}function ue(e,t,n){e.forEach(function(e){e[t]&&e[t].unique&&y(e[t].cell,n)})}function ce(e,t,n,a){Array.isArray(t)||(t=de(t));for(var r,o=0,i=e.aoColumns,l=0,s=t.length;l<s;l++){var u=i[t[l]],c=n?u.sWidthOrig:u.sWidth;if(a||!1!==u.bVisible){if(null==c)return null;"number"==typeof c?(r="px",o+=c):(u=c.match(/([\d\.]+)([^\d]*)/))&&(o+=+u[1],r=3===u.length?u[2]:"px")}}return o+r}function de(e){e=H(e).closest("[data-dt-column]").attr("data-dt-column");return e?e.split(",").map(function(e){return+e}):[]}function D(e,t,n,a){for(var r=e.aoData.length,o=H.extend(!0,{},V.models.oRow,{src:n?"dom":"data",idx:r}),i=(o._aData=t,e.aoData.push(o),e.aoColumns),l=0,s=i.length;l<s;l++)i[l].sType=null;e.aiDisplayMaster.push(r);t=e.rowIdFn(t);return void 0!==t&&(e.aIds[t]=o),!n&&e.oFeatures.bDeferRender||xe(e,r,n,a),r}function fe(n,e){var a;return(e=e instanceof H?e:H(e)).map(function(e,t){return a=ye(n,t),D(n,a.data,t,a.cells)})}function B(e,t,n,a){"search"===a?a="filter":"order"===a&&(a="sort");var r=e.aoData[t];if(r){var o=e.iDraw,i=e.aoColumns[n],r=r._aData,l=i.sDefaultContent,s=i.fnGetData(r,a,{settings:e,row:t,col:n});if(void 0===(s="display"!==a&&s&&"object"==typeof s&&s.nodeName?s.innerHTML:s))return e.iDrawError!=o&&null===l&&($(e,0,"Requested unknown parameter "+("function"==typeof i.mData?"{function}":"'"+i.mData+"'")+" for row "+t+", column "+n,4),e.iDrawError=o),l;if(s!==r&&null!==s||null===l||void 0===a){if("function"==typeof s)return s.call(r)}else s=l;return null===s&&"display"===a?"":s="filter"===a&&(t=V.ext.type.search)[i.sType]?t[i.sType](s):s}}function he(e,t){t&&"object"==typeof t&&t.nodeName?H(e).empty().append(t):e.innerHTML=t}var pe=/\[.*?\]$/,p=/\(\)$/;function ge(e){return(e.match(/(\\.|[^.])+/g)||[""]).map(function(e){return e.replace(/\\\./g,".")})}var U=V.util.get,v=V.util.set;function ve(e){return m(e.aoData,"_aData")}function me(e){e.aoData.length=0,e.aiDisplayMaster.length=0,e.aiDisplay.length=0,e.aIds={}}function be(e,t,n,a){var r,o,i=e.aoData[t];if(i._aSortData=null,i._aFilterData=null,i.displayData=null,"dom"!==n&&(n&&"auto"!==n||"dom"!==i.src)){var l=i.anCells,s=De(e,t);if(l)if(void 0!==a)he(l[a],s[a]);else for(r=0,o=l.length;r<o;r++)he(l[r],s[r])}else i._aData=ye(e,i,a,void 0===a?void 0:i._aData).data;var u=e.aoColumns;if(void 0!==a)u[a].sType=null,u[a].maxLenString=null;else{for(r=0,o=u.length;r<o;r++)u[r].sType=null,u[r].maxLenString=null;Se(e,i)}}function ye(e,t,n,a){function r(e,t){var n;"string"==typeof e&&-1!==(n=e.indexOf("@"))&&(n=e.substring(n+1),v(e)(a,t.getAttribute(n)))}function o(e){void 0!==n&&n!==d||(l=f[d],s=e.innerHTML.trim(),l&&l._bAttrSrc?(v(l.mData._)(a,s),r(l.mData.sort,e),r(l.mData.type,e),r(l.mData.filter,e)):h?(l._setter||(l._setter=v(l.mData)),l._setter(a,s)):a[d]=s),d++}var i,l,s,u=[],c=t.firstChild,d=0,f=e.aoColumns,h=e._rowReadObject;a=void 0!==a?a:h?{}:[];if(c)for(;c;)"TD"!=(i=c.nodeName.toUpperCase())&&"TH"!=i||(o(c),u.push(c)),c=c.nextSibling;else for(var p=0,g=(u=t.anCells).length;p<g;p++)o(u[p]);var t=t.firstChild?t:t.nTr;return t&&(t=t.getAttribute("id"))&&v(e.rowId)(a,t),{data:a,cells:u}}function De(e,t){var n=e.aoData[t],a=e.aoColumns;if(!n.displayData){n.displayData=[];for(var r=0,o=a.length;r<o;r++)n.displayData.push(B(e,t,r,"display"))}return n.displayData}function xe(e,t,n,a){var r,o,i,l,s,u,c=e.aoData[t],d=c._aData,f=[],h=e.oClasses.tbody.row;if(null===c.nTr){for(r=n||_.createElement("tr"),c.nTr=r,c.anCells=f,y(r,h),r._DT_RowIndex=t,Se(e,c),l=0,s=e.aoColumns.length;l<s;l++){i=e.aoColumns[l],(o=(u=!n||!a[l])?_.createElement(i.sCellType):a[l])||$(e,0,"Incorrect column count",18),o._DT_CellIndex={row:t,column:l},f.push(o);var p=De(e,t);!u&&(!i.mRender&&i.mData===l||H.isPlainObject(i.mData)&&i.mData._===l+".display")||he(o,p[l]),i.bVisible&&u?r.appendChild(o):i.bVisible||u||o.parentNode.removeChild(o),i.fnCreatedCell&&i.fnCreatedCell.call(e.oInstance,o,B(e,t,l),d,t,l)}G(e,"aoRowCreatedCallback","row-created",[r,d,t,f])}else y(c.nTr,h)}function Se(e,t){var n=t.nTr,a=t._aData;n&&((e=e.rowIdFn(a))&&(n.id=e),a.DT_RowClass&&(e=a.DT_RowClass.split(" "),t.__rowc=t.__rowc?x(t.__rowc.concat(e)):e,H(n).removeClass(t.__rowc.join(" ")).addClass(a.DT_RowClass)),a.DT_RowAttr&&H(n).attr(a.DT_RowAttr),a.DT_RowData)&&H(n).data(a.DT_RowData)}function we(e,t){var n,a=e.oClasses,r=e.aoColumns,o="header"===t?e.nTHead:e.nTFoot,i="header"===t?"sTitle":t;if(o){if(("header"===t||m(e.aoColumns,i).join(""))&&1===(n=(n=H("tr",o)).length?n:H("<tr/>").appendTo(o)).length)for(var l=H("td, th",n).length,s=r.length;l<s;l++)H("<th/>").html(r[l][i]||"").appendTo(n);var u=Ae(e,o,!0);"header"===t?e.aoHeader=u:e.aoFooter=u,H(o).children("tr").attr("role","row"),H(o).children("tr").children("th, td").each(function(){at(e,t)(e,H(this),a)})}}function Te(e,t,n){var a,r,o,i,l,s=[],u=[],c=e.aoColumns,e=c.length;if(t){for(n=n||h(e).filter(function(e){return c[e].bVisible}),a=0;a<t.length;a++)s[a]=t[a].slice().filter(function(e,t){return n.includes(t)}),u.push([]);for(a=0;a<s.length;a++)for(r=0;r<s[a].length;r++)if(l=i=1,void 0===u[a][r]){for(o=s[a][r].cell;void 0!==s[a+i]&&s[a][r].cell==s[a+i][r].cell;)u[a+i][r]=null,i++;for(;void 0!==s[a][r+l]&&s[a][r].cell==s[a][r+l].cell;){for(var d=0;d<i;d++)u[a+d][r+l]=null;l++}var f=H("span.dt-column-title",o);u[a][r]={cell:o,colspan:l,rowspan:i,title:(f.length?f:H(o)).html()}}return u}}function _e(e,t){for(var n,a,r=Te(e,t),o=0;o<t.length;o++){if(n=t[o].row)for(;a=n.firstChild;)n.removeChild(a);for(var i=0;i<r[o].length;i++){var l=r[o][i];l&&H(l.cell).appendTo(n).attr("rowspan",l.rowspan).attr("colspan",l.colspan)}}}function S(e,t){if(r="ssp"==J(s=e),void 0!==(i=s.iInitDisplayStart)&&-1!==i&&(s._iDisplayStart=!r&&i>=s.fnRecordsDisplay()?0:i,s.iInitDisplayStart=-1),-1!==G(e,"aoPreDrawCallback","preDraw",[e]).indexOf(!1))w(e,!1);else{var l,n=[],a=0,r="ssp"==J(e),o=e.aiDisplay,i=e._iDisplayStart,s=e.fnDisplayEnd(),u=e.aoColumns,c=H(e.nTBody);if(e.bDrawing=!0,e.deferLoading)e.deferLoading=!1,e.iDraw++,w(e,!1);else if(r){if(!e.bDestroying&&!t)return 0===e.iDraw&&c.empty().append(Ce(e)),(l=e).iDraw++,w(l,!0),void Fe(l,function(t){function n(e,t){return"function"==typeof a[e][t]?"function":a[e][t]}var a=t.aoColumns,e=t.oFeatures,r=t.oPreviousSearch,o=t.aoPreSearchCols;return{draw:t.iDraw,columns:a.map(function(t,e){return{data:n(e,"mData"),name:t.sName,searchable:t.bSearchable,orderable:t.bSortable,search:{value:o[e].search,regex:o[e].regex,fixed:Object.keys(t.searchFixed).map(function(e){return{name:e,term:t.searchFixed[e].toString()}})}}}),order:Ge(t).map(function(e){return{column:e.col,dir:e.dir,name:n(e.col,"sName")}}),start:t._iDisplayStart,length:e.bPaginate?t._iDisplayLength:-1,search:{value:r.search,regex:r.regex,fixed:Object.keys(t.searchFixed).map(function(e){return{name:e,term:t.searchFixed[e].toString()}})}}}(l),function(e){var t=l,n=Ne(t,e=e),a=je(t,"draw",e),r=je(t,"recordsTotal",e),e=je(t,"recordsFiltered",e);if(void 0!==a){if(+a<t.iDraw)return;t.iDraw=+a}n=n||[],me(t),t._iRecordsTotal=parseInt(r,10),t._iRecordsDisplay=parseInt(e,10);for(var o=0,i=n.length;o<i;o++)D(t,n[o]);t.aiDisplay=t.aiDisplayMaster.slice(),S(t,!0),He(t),w(t,!1)})}else e.iDraw++;if(0!==o.length)for(var d=r?e.aoData.length:s,f=r?0:i;f<d;f++){for(var h=o[f],p=e.aoData[h],g=(null===p.nTr&&xe(e,h),p.nTr),v=0;v<u.length;v++){var m=u[v],b=p.anCells[v];y(b,C.type.className[m.sType]),y(b,m.sClass),y(b,e.oClasses.tbody.cell)}G(e,"aoRowCallback",null,[g,p._aData,a,f,h]),n.push(g),a++}else n[0]=Ce(e);G(e,"aoHeaderCallback","header",[H(e.nTHead).children("tr")[0],ve(e),i,s,o]),G(e,"aoFooterCallback","footer",[H(e.nTFoot).children("tr")[0],ve(e),i,s,o]),c[0].replaceChildren?c[0].replaceChildren.apply(c[0],n):(c.children().detach(),c.append(H(n))),H(e.nTableWrapper).toggleClass("dt-empty-footer",0===H("tr",e.nTFoot).length),G(e,"aoDrawCallback","draw",[e],!0),e.bSorted=!1,e.bFiltered=!1,e.bDrawing=!1}}function d(e,t,n){var a=e.oFeatures,r=a.bSort,a=a.bFilter;void 0!==n&&!0!==n||(se(e),r&&Je(e),a?Re(e,e.oPreviousSearch):e.aiDisplay=e.aiDisplayMaster.slice()),!0!==t&&(e._iDisplayStart=0),e._drawHold=t,S(e),e._drawHold=!1}function Ce(e){var t=e.oLanguage,n=t.sZeroRecords,a=J(e);return e.iDraw<1&&"ssp"===a||e.iDraw<=1&&"ajax"===a?n=t.sLoadingRecords:t.sEmptyTable&&0===e.fnRecordsTotal()&&(n=t.sEmptyTable),H("<tr/>").append(H("<td />",{colSpan:oe(e),class:e.oClasses.empty.row}).html(n))[0]}function Le(e,t,r){var o=[];H.each(t,function(e,t){var n,a;null!==t&&(n=(e=e.match(/^([a-z]+)([0-9]*)([A-Za-z]*)$/))[2]?+e[2]:0,a=e[3]?e[3].toLowerCase():"full",e[1]===r)&&function e(t,n,a){if(Array.isArray(a))for(var r=0;r<a.length;r++)e(t,n,a[r]);else{var o=t[n];H.isPlainObject(a)?a.features?(a.rowId&&(t.id=a.rowId),a.rowClass&&(t.className=a.rowClass),o.id=a.id,o.className=a.className,e(t,n,a.features)):Object.keys(a).map(function(e){o.contents.push({feature:e,opts:a[e]})}):o.contents.push(a)}}(function(e,t,n){for(var a,r=0;r<e.length;r++)if((a=e[r]).rowNum===t&&("full"===n&&a.full||("start"===n||"end"===n)&&(a.start||a.end)))return a[n]||(a[n]={contents:[]}),a;return(a={rowNum:t})[n]={contents:[]},e.push(a),a}(o,n,a),a,t)}),o.sort(function(e,t){var n=e.rowNum,a=t.rowNum;return n===a?(e=e.full&&!t.full?-1:1,"bottom"===r?-1*e:e):a-n}),"bottom"===r&&o.reverse();for(var n=0;n<o.length;n++)delete o[n].rowNum,!function(o,i){function l(e,t){return C.features[e]||$(o,0,"Unknown feature: "+e),C.features[e].apply(this,[o,t])}function e(e){if(i[e])for(var t,n=i[e].contents,a=0,r=n.length;a<r;a++)n[a]&&("string"==typeof n[a]?n[a]=l(n[a],null):H.isPlainObject(n[a])?n[a]=l(n[a].feature,n[a].opts):"function"==typeof n[a].node?n[a]=n[a].node(o):"function"==typeof n[a]&&(t=n[a](o),n[a]="function"==typeof t.node?t.node():t))}e("start"),e("end"),e("full")}(e,o[n]);return o}function Ie(t){var a,e=t.oClasses,n=H(t.nTable),r=H("<div/>").attr({id:t.sTableId+"_wrapper",class:e.container}).insertBefore(n);if(t.nTableWrapper=r[0],t.sDom)for(var o,i,l,s,u,c,d=t,e=t.sDom,f=r,h=e.match(/(".*?")|('.*?')|./g),p=0;p<h.length;p++)o=null,"<"==(i=h[p])?(l=H("<div/>"),"'"!=(s=h[p+1])[0]&&'"'!=s[0]||(s=s.replace(/['"]/g,""),u="",-1!=s.indexOf(".")?(c=s.split("."),u=c[0],c=c[1]):"#"==s[0]?u=s:c=s,l.attr("id",u.substring(1)).addClass(c),p++),f.append(l),f=l):">"==i?f=f.parent():"t"==i?o=qe(d):V.ext.feature.forEach(function(e){i==e.cFeature&&(o=e.fnInit(d))}),o&&f.append(o);else{var n=Le(t,t.layout,"top"),e=Le(t,t.layout,"bottom"),g=at(t,"layout");n.forEach(function(e){g(t,r,e)}),g(t,r,{full:{table:!0,contents:[qe(t)]}}),e.forEach(function(e){g(t,r,e)})}var n=t,e=n.nTable,v=""!==n.oScroll.sX||""!==n.oScroll.sY;n.oFeatures.bProcessing&&(a=H("<div/>",{id:n.sTableId+"_processing",class:n.oClasses.processing.container,role:"status"}).html(n.oLanguage.sProcessing).append("<div><div></div><div></div><div></div><div></div></div>"),v?a.prependTo(H("div.dt-scroll",n.nTableWrapper)):a.insertBefore(e),H(e).on("processing.dt.DT",function(e,t,n){a.css("display",n?"block":"none")}))}function Ae(e,t,n){for(var a,r,o,i,l,s,u=e.aoColumns,c=H(t).children("tr"),d=t&&"thead"===t.nodeName.toLowerCase(),f=[],h=0,p=c.length;h<p;h++)f.push([]);for(h=0,p=c.length;h<p;h++)for(r=(a=c[h]).firstChild;r;){if("TD"==r.nodeName.toUpperCase()||"TH"==r.nodeName.toUpperCase()){var g,v,m,b,y,D=[];for(b=(b=+r.getAttribute("colspan"))&&0!=b&&1!=b?b:1,y=(y=+r.getAttribute("rowspan"))&&0!=y&&1!=y?y:1,l=function(e,t,n){for(var a=e[t];a[n];)n++;return n}(f,h,0),s=1==b,n&&(s&&(te(e,l,H(r).data()),g=u[l],v=r.getAttribute("width")||null,(m=r.style.width.match(/width:\s*(\d+[pxem%]+)/))&&(v=m[1]),g.sWidthOrig=g.sWidth||v,d?(null===g.sTitle||g.autoTitle||(r.innerHTML=g.sTitle),!g.sTitle&&s&&(g.sTitle=L(r.innerHTML),g.autoTitle=!0)):g.footer&&(r.innerHTML=g.footer),g.ariaTitle||(g.ariaTitle=H(r).attr("aria-label")||g.sTitle),g.className)&&H(r).addClass(g.className),0===H("span.dt-column-title",r).length&&H("<span>").addClass("dt-column-title").append(r.childNodes).appendTo(r),d)&&0===H("span.dt-column-order",r).length&&H("<span>").addClass("dt-column-order").appendTo(r),i=0;i<b;i++){for(o=0;o<y;o++)f[h+o][l+i]={cell:r,unique:s},f[h+o].row=a;D.push(l+i)}r.setAttribute("data-dt-column",x(D).join(","))}r=r.nextSibling}return f}function Fe(n,e,a){function t(e){var t=n.jqXHR?n.jqXHR.status:null;if((null===e||"number"==typeof t&&204==t)&&Ne(n,e={},[]),(t=e.error||e.sError)&&$(n,0,t),e.d&&"string"==typeof e.d)try{e=JSON.parse(e.d)}catch(e){}n.json=e,G(n,null,"xhr",[n,e,n.jqXHR],!0),a(e)}var r,o=n.ajax,i=n.oInstance,l=(H.isPlainObject(o)&&o.data&&(l="function"==typeof(r=o.data)?r(e,n):r,e="function"==typeof r&&l?l:H.extend(!0,e,l),delete o.data),{url:"string"==typeof o?o:"",data:e,success:t,dataType:"json",cache:!1,type:n.sServerMethod,error:function(e,t){-1===G(n,null,"xhr",[n,null,n.jqXHR],!0).indexOf(!0)&&("parsererror"==t?$(n,0,"Invalid JSON response",1):4===e.readyState&&$(n,0,"Ajax error",7)),w(n,!1)}});H.isPlainObject(o)&&H.extend(l,o),n.oAjaxData=e,G(n,null,"preXhr",[n,e,l],!0),"function"==typeof o?n.jqXHR=o.call(i,e,t,n):""===o.url?(i={},V.util.set(o.dataSrc)(i,[]),t(i)):n.jqXHR=H.ajax(l),r&&(o.data=r)}function Ne(e,t,n){var a="data";if(H.isPlainObject(e.ajax)&&void 0!==e.ajax.dataSrc&&("string"==typeof(e=e.ajax.dataSrc)||"function"==typeof e?a=e:void 0!==e.data&&(a=e.data)),!n)return"data"===a?t.aaData||t[a]:""!==a?U(a)(t):t;v(a)(t,n)}function je(e,t,n){var e=H.isPlainObject(e.ajax)?e.ajax.dataSrc:null;return e&&e[t]?U(e[t])(n):(e="","draw"===t?e="sEcho":"recordsTotal"===t?e="iTotalRecords":"recordsFiltered"===t&&(e="iTotalDisplayRecords"),void 0!==n[e]?n[e]:n[t])}function Re(n,e){var t=n.aoPreSearchCols;if("ssp"!=J(n)){for(var a,r,o,i,l,s=n,u=s.aoColumns,c=s.aoData,d=0;d<c.length;d++)if(c[d]&&!(l=c[d])._aFilterData){for(o=[],a=0,r=u.length;a<r;a++)u[a].bSearchable?"string"!=typeof(i=null===(i=B(s,d,a,"filter"))?"":i)&&i.toString&&(i=i.toString()):i="",i.indexOf&&-1!==i.indexOf("&")&&(Ee.innerHTML=i,i=ke?Ee.textContent:Ee.innerText),i.replace&&(i=i.replace(/[\r\n\u2028]/g,"")),o.push(i);l._aFilterData=o,l._sFilterRow=o.join(" "),0}n.aiDisplay=n.aiDisplayMaster.slice(),Oe(n.aiDisplay,n,e.search,e),H.each(n.searchFixed,function(e,t){Oe(n.aiDisplay,n,t,{})});for(var f=0;f<t.length;f++){var h=t[f];Oe(n.aiDisplay,n,h.search,h,f),H.each(n.aoColumns[f].searchFixed,function(e,t){Oe(n.aiDisplay,n,t,{},f)})}for(var p,g,v=n,m=V.ext.search,b=v.aiDisplay,y=0,D=m.length;y<D;y++){for(var x=[],S=0,w=b.length;S<w;S++)g=b[S],p=v.aoData[g],m[y](v,p._aFilterData,g,p._aData,S)&&x.push(g);b.length=0,b.push.apply(b,x)}}n.bFiltered=!0,G(n,null,"search",[n])}function Oe(e,t,n,a,r){if(""!==n){for(var o=0,i=[],l="function"==typeof n?n:null,s=n instanceof RegExp?n:l?null:function(e,t){var a=[],t=H.extend({},{boundary:!1,caseInsensitive:!0,exact:!1,regex:!1,smart:!0},t);"string"!=typeof e&&(e=e.toString());if(e=k(e),t.exact)return new RegExp("^"+Pe(e)+"$",t.caseInsensitive?"i":"");{var n,r,o;e=t.regex?e:Pe(e),t.smart&&(n=(e.match(/!?["\u201C][^"\u201D]+["\u201D]|[^ ]+/g)||[""]).map(function(e){var t,n=!1;return"!"===e.charAt(0)&&(n=!0,e=e.substring(1)),'"'===e.charAt(0)?e=(t=e.match(/^"(.*)"$/))?t[1]:e:"“"===e.charAt(0)&&(e=(t=e.match(/^\u201C(.*)\u201D$/))?t[1]:e),n&&(1<e.length&&a.push("(?!"+e+")"),e=""),e.replace(/"/g,"")}),r=a.length?a.join(""):"",o=t.boundary?"\\b":"",e="^(?=.*?"+o+n.join(")(?=.*?"+o)+")("+r+".)*$")}return new RegExp(e,t.caseInsensitive?"i":"")}(n,a),o=0;o<e.length;o++){var u=t.aoData[e[o]],c=void 0===r?u._sFilterRow:u._aFilterData[r];(l&&l(c,u._aData,e[o],r)||s&&s.test(c))&&i.push(e[o])}for(e.length=i.length,o=0;o<i.length;o++)e[o]=i[o]}}var Pe=V.util.escapeRegex,Ee=H("<div>")[0],ke=void 0!==Ee.textContent;function Me(i){var l,t,n,e,s=i.oInit,u=i.deferLoading,c=J(i);i.bInitialised?(we(i,"header"),we(i,"footer"),n=function(){_e(i,i.aoHeader),_e(i,i.aoFooter);var n=i.iInitDisplayStart;if(s.aaData)for(l=0;l<s.aaData.length;l++)D(i,s.aaData[l]);else!u&&"dom"!=c||fe(i,H(i.nTBody).children("tr"));i.aiDisplay=i.aiDisplayMaster.slice(),Ie(i);var e=i,t=e.nTHead,a=t.querySelectorAll("tr"),r=e.bSortCellsTop,o=':not([data-dt-order="disable"]):not([data-dt-order="icon-only"])';!0===r?t=a[0]:!1===r&&(t=a[a.length-1]),$e(e,t,t===e.nTHead?"tr"+o+" th"+o+", tr"+o+" td"+o:"th"+o+", td"+o),Ye(e,r=[],e.aaSorting),e.aaSorting=r,Ue(i),w(i,!0),G(i,null,"preInit",[i],!0),d(i),"ssp"==c&&!u||("ajax"==c?Fe(i,{},function(e){var t=Ne(i,e);for(l=0;l<t.length;l++)D(i,t[l]);i.iInitDisplayStart=n,d(i),w(i,!1),He(i)}):(He(i),w(i,!1)))},(t=i).oFeatures.bStateSave?void 0!==(e=t.fnStateLoadCallback.call(t.oInstance,t,function(e){Ke(t,e,n)}))&&Ke(t,e,n):n()):setTimeout(function(){Me(i)},200)}function He(e){var t;e._bInitComplete||(t=[e,e.json],e._bInitComplete=!0,ne(e),G(e,null,"plugin-init",t,!0),G(e,"aoInitComplete","init",t,!0))}function We(e,t){t=parseInt(t,10);e._iDisplayLength=t,nt(e),G(e,null,"length",[e,t])}function Xe(e,t,n){var a=e._iDisplayStart,r=e._iDisplayLength,o=e.fnRecordsDisplay();if(0===o||-1===r)a=0;else if("number"==typeof t)o<(a=t*r)&&(a=0);else if("first"==t)a=0;else if("previous"==t)(a=0<=r?a-r:0)<0&&(a=0);else if("next"==t)a+r<o&&(a+=r);else if("last"==t)a=Math.floor((o-1)/r)*r;else{if("ellipsis"===t)return;$(e,0,"Unknown paging action: "+t,5)}o=e._iDisplayStart!==a;e._iDisplayStart=a,G(e,null,o?"page":"page-nc",[e]),o&&n&&S(e)}function w(e,t){e.bDrawing&&!1===t||G(e,null,"processing",[e,t])}function Ve(e,t,n){t?(w(e,!0),setTimeout(function(){n(),w(e,!1)},0)):n()}function qe(e){var t,n,a,r,o,i,l,s,u,c,d,f,h,p=H(e.nTable),g=e.oScroll;return""===g.sX&&""===g.sY?e.nTable:(t=g.sX,n=g.sY,a=e.oClasses.scrolling,o=(r=e.captionNode)?r._captionSide:null,u=H(p[0].cloneNode(!1)),i=H(p[0].cloneNode(!1)),c=function(e){return e?I(e):null},(l=p.children("tfoot")).length||(l=null),u=H(s="<div/>",{class:a.container}).append(H(s,{class:a.header.self}).css({overflow:"hidden",position:"relative",border:0,width:t?c(t):"100%"}).append(H(s,{class:a.header.inner}).css({"box-sizing":"content-box",width:g.sXInner||"100%"}).append(u.removeAttr("id").css("margin-left",0).append("top"===o?r:null).append(p.children("thead"))))).append(H(s,{class:a.body}).css({position:"relative",overflow:"auto",width:c(t)}).append(p)),l&&u.append(H(s,{class:a.footer.self}).css({overflow:"hidden",border:0,width:t?c(t):"100%"}).append(H(s,{class:a.footer.inner}).append(i.removeAttr("id").css("margin-left",0).append("bottom"===o?r:null).append(p.children("tfoot"))))),c=u.children(),d=c[0],f=c[1],h=l?c[2]:null,H(f).on("scroll.DT",function(){var e=this.scrollLeft;d.scrollLeft=e,l&&(h.scrollLeft=e)}),H("th, td",d).on("focus",function(){var e=d.scrollLeft;f.scrollLeft=e,l&&(f.scrollLeft=e)}),H(f).css("max-height",n),g.bCollapse||H(f).css("height",n),e.nScrollHead=d,e.nScrollBody=f,e.nScrollFoot=h,e.aoDrawCallback.push(Be),u[0])}function Be(t){var e=t.oScroll.iBarWidth,n=H(t.nScrollHead).children("div"),a=n.children("table"),r=t.nScrollBody,o=H(r),i=H(t.nScrollFoot).children("div"),l=i.children("table"),s=H(t.nTHead),u=H(t.nTable),c=t.nTFoot&&H("th, td",t.nTFoot).length?H(t.nTFoot):null,d=t.oBrowser,f=r.scrollHeight>r.clientHeight;if(t.scrollBarVis!==f&&void 0!==t.scrollBarVis)t.scrollBarVis=f,ne(t);else{if(t.scrollBarVis=f,u.children("thead, tfoot").remove(),(f=s.clone().prependTo(u)).find("th, td").removeAttr("tabindex"),f.find("[id]").removeAttr("id"),c&&(v=c.clone().prependTo(u)).find("[id]").removeAttr("id"),t.aiDisplay.length)for(var h=u.children("tbody").eq(0).children("tr").eq(0).children("th, td").map(function(e){return{idx:ae(t,e),width:H(this).outerWidth()}}),p=0;p<h.length;p++){var g=t.aoColumns[h[p].idx].colEl[0];g.style.width.replace("px","")!==h[p].width&&(g.style.width=h[p].width+"px")}a.find("colgroup").remove(),a.append(t.colgroup.clone()),c&&(l.find("colgroup").remove(),l.append(t.colgroup.clone())),H("th, td",f).each(function(){H(this.childNodes).wrapAll('<div class="dt-scroll-sizing">')}),c&&H("th, td",v).each(function(){H(this.childNodes).wrapAll('<div class="dt-scroll-sizing">')});var s=Math.floor(u.height())>r.clientHeight||"scroll"==o.css("overflow-y"),f="padding"+(d.bScrollbarLeft?"Left":"Right"),v=u.outerWidth();a.css("width",I(v)),n.css("width",I(v)).css(f,s?e+"px":"0px"),c&&(l.css("width",I(v)),i.css("width",I(v)).css(f,s?e+"px":"0px")),u.children("colgroup").prependTo(u),o.trigger("scroll"),!t.bSorted&&!t.bFiltered||t._drawHold||(r.scrollTop=0)}}function I(e){return null===e?"0px":"number"==typeof e?e<0?"0px":e+"px":e.match(/\d$/)?e+"px":e}function Ue(e){var t=e.aoColumns;for(e.colgroup.empty(),a=0;a<t.length;a++)t[a].bVisible&&e.colgroup.append(t[a].colEl)}function $e(o,e,t,i,l){tt(e,t,function(e){var t=!1,n=void 0===i?de(e.target):[i];if(n.length){for(var a=0,r=n.length;a<r;a++)if(!1!==function(e,t,n,a){function r(e,t){var n=e._idx;return(n=void 0===n?s.indexOf(e[1]):n)+1<s.length?n+1:t?null:0}var o,i=e.aoColumns[t],l=e.aaSorting,s=i.asSorting;if(!i.bSortable)return!1;"number"==typeof l[0]&&(l=e.aaSorting=[l]);(a||n)&&e.oFeatures.bSortMulti?-1!==(i=m(l,"0").indexOf(t))?null===(o=null===(o=r(l[i],!0))&&1===l.length?0:o)?l.splice(i,1):(l[i][1]=s[o],l[i]._idx=o):(a?l.push([t,s[0],0]):l.push([t,l[0][1],0]),l[l.length-1]._idx=0):l.length&&l[0][0]==t?(o=r(l[0]),l.length=1,l[0][1]=s[o],l[0]._idx=o):(l.length=0,l.push([t,s[0]]),l[0]._idx=0)}(o,n[a],a,e.shiftKey)&&(t=!0),1===o.aaSorting.length&&""===o.aaSorting[0][1])break;t&&Ve(o,!0,function(){Je(o),ze(o,o.aiDisplay),d(o,!1,!1),l&&l()})}})}function ze(e,t){if(!(t.length<2)){for(var n=e.aiDisplayMaster,a={},r={},o=0;o<n.length;o++)a[n[o]]=o;for(o=0;o<t.length;o++)r[t[o]]=a[t[o]];t.sort(function(e,t){return r[e]-r[t]})}}function Ye(n,a,e){function t(e){var t;H.isPlainObject(e)?void 0!==e.idx?a.push([e.idx,e.dir]):e.name&&-1!==(t=m(n.aoColumns,"sName").indexOf(e.name))&&a.push([t,e.dir]):a.push(e)}if(H.isPlainObject(e))t(e);else if(e.length&&"number"==typeof e[0])t(e);else if(e.length)for(var r=0;r<e.length;r++)t(e[r])}function Ge(e){var t,n,a,r,o,i,l,s=[],u=V.ext.type.order,c=e.aoColumns,d=e.aaSortingFixed,f=H.isPlainObject(d),h=[];if(e.oFeatures.bSort)for(Array.isArray(d)&&Ye(e,h,d),f&&d.pre&&Ye(e,h,d.pre),Ye(e,h,e.aaSorting),f&&d.post&&Ye(e,h,d.post),t=0;t<h.length;t++)if(c[l=h[t][0]])for(n=0,a=(r=c[l].aDataSort).length;n<a;n++)i=c[o=r[n]].sType||"string",void 0===h[t]._idx&&(h[t]._idx=c[o].asSorting.indexOf(h[t][1])),h[t][1]&&s.push({src:l,col:o,dir:h[t][1],index:h[t]._idx,type:i,formatter:u[i+"-pre"],sorter:u[i+"-"+h[t][1]]});return s}function Je(e,t,n){var a,r,o,i,l,c,d=[],s=V.ext.type.order,f=e.aoData,u=e.aiDisplayMaster;for(void 0!==t?(l=e.aoColumns[t],c=[{src:t,col:t,dir:n,index:0,type:l.sType,formatter:s[l.sType+"-pre"],sorter:s[l.sType+"-"+n]}],u=u.slice()):c=Ge(e),a=0,r=c.length;a<r;a++){i=c[a],S=x=D=g=p=h=y=b=m=v=void 0;var h,p,g,v=e,m=i.col,b=v.aoColumns[m],y=V.ext.order[b.sSortDataType];y&&(h=y.call(v.oInstance,v,m,re(v,m)));for(var D=V.ext.type.order[b.sType+"-pre"],x=v.aoData,S=0;S<x.length;S++)x[S]&&((p=x[S])._aSortData||(p._aSortData=[]),p._aSortData[m]&&!y||(g=y?h[S]:B(v,S,m,"sort"),p._aSortData[m]=D?D(g,v):g))}if("ssp"!=J(e)&&0!==c.length){for(a=0,o=u.length;a<o;a++)d[a]=a;c.length&&"desc"===c[0].dir&&e.orderDescReverse&&d.reverse(),u.sort(function(e,t){for(var n,a,r,o,i=c.length,l=f[e]._aSortData,s=f[t]._aSortData,u=0;u<i;u++)if(n=l[(o=c[u]).col],a=s[o.col],o.sorter){if(0!==(r=o.sorter(n,a)))return r}else if(0!==(r=n<a?-1:a<n?1:0))return"asc"===o.dir?r:-r;return(n=d[e])<(a=d[t])?-1:a<n?1:0})}else 0===c.length&&u.sort(function(e,t){return e<t?-1:t<e?1:0});return void 0===t&&(e.bSorted=!0,e.sortDetails=c,G(e,null,"order",[e,c])),u}function Ze(e){var t,n,a,r=e.aLastSort,o=e.oClasses.order.position,i=Ge(e),l=e.oFeatures;if(l.bSort&&l.bSortClasses){for(t=0,n=r.length;t<n;t++)a=r[t].src,H(m(e.aoData,"anCells",a)).removeClass(o+(t<2?t+1:3));for(t=0,n=i.length;t<n;t++)a=i[t].src,H(m(e.aoData,"anCells",a)).addClass(o+(t<2?t+1:3))}e.aLastSort=i}function Qe(n){var e;n._bLoadingState||(e={time:+new Date,start:n._iDisplayStart,length:n._iDisplayLength,order:H.extend(!0,[],n.aaSorting),search:H.extend({},n.oPreviousSearch),columns:n.aoColumns.map(function(e,t){return{visible:e.bVisible,search:H.extend({},n.aoPreSearchCols[t])}})},n.oSavedState=e,G(n,"aoStateSaveParams","stateSaveParams",[n,e]),n.oFeatures.bStateSave&&!n.bDestroying&&n.fnStateSaveCallback.call(n.oInstance,n,e))}function Ke(n,e,t){var a,r,o=n.aoColumns,i=(n._bLoadingState=!0,n._bInitComplete?new V.Api(n):null);if(e&&e.time){var l=n.iStateDuration;if(0<l&&e.time<+new Date-1e3*l)n._bLoadingState=!1;else if(-1!==G(n,"aoStateLoadParams","stateLoadParams",[n,e]).indexOf(!1))n._bLoadingState=!1;else if(e.columns&&o.length!==e.columns.length)n._bLoadingState=!1;else{if(n.oLoadedState=H.extend(!0,{},e),G(n,null,"stateLoadInit",[n,e],!0),void 0!==e.length&&(i?i.page.len(e.length):n._iDisplayLength=e.length),void 0!==e.start&&(null===i?(n._iDisplayStart=e.start,n.iInitDisplayStart=e.start):Xe(n,e.start/n._iDisplayLength)),void 0!==e.order&&(n.aaSorting=[],H.each(e.order,function(e,t){n.aaSorting.push(t[0]>=o.length?[0,t[1]]:t)})),void 0!==e.search&&H.extend(n.oPreviousSearch,e.search),e.columns){for(a=0,r=e.columns.length;a<r;a++){var s=e.columns[a];void 0!==s.visible&&(i?i.column(a).visible(s.visible,!1):o[a].bVisible=s.visible),void 0!==s.search&&H.extend(n.aoPreSearchCols[a],s.search)}i&&i.columns.adjust()}n._bLoadingState=!1,G(n,"aoStateLoaded","stateLoaded",[n,e])}}else n._bLoadingState=!1;t()}function $(e,t,n,a){if(n="DataTables warning: "+(e?"table id="+e.sTableId+" - ":"")+n,a&&(n+=". For more information about this error, please see https://datatables.net/tn/"+a),t)W.console&&console.log&&console.log(n);else{t=V.ext,t=t.sErrMode||t.errMode;if(e&&G(e,null,"dt-error",[e,a,n],!0),"alert"==t)alert(n);else{if("throw"==t)throw new Error(n);"function"==typeof t&&t(e,a,n)}}}function z(n,a,e,t){Array.isArray(e)?H.each(e,function(e,t){Array.isArray(t)?z(n,a,t[0],t[1]):z(n,a,t)}):(void 0===t&&(t=e),void 0!==a[e]&&(n[t]=a[e]))}function et(e,t,n){var a,r;for(r in t)Object.prototype.hasOwnProperty.call(t,r)&&(a=t[r],H.isPlainObject(a)?(H.isPlainObject(e[r])||(e[r]={}),H.extend(!0,e[r],a)):n&&"data"!==r&&"aaData"!==r&&Array.isArray(a)?e[r]=a.slice():e[r]=a);return e}function tt(e,t,n){H(e).on("click.DT",t,function(e){n(e)}).on("keypress.DT",t,function(e){13===e.which&&(e.preventDefault(),n(e))}).on("selectstart.DT",t,function(){return!1})}function Y(e,t,n){n&&e[t].push(n)}function G(t,e,n,a,r){var o=[];return e&&(o=t[e].slice().reverse().map(function(e){return e.apply(t.oInstance,a)})),null!==n&&(e=H.Event(n+".dt"),n=H(t.nTable),e.dt=t.api,n[r?"trigger":"triggerHandler"](e,a),r&&0===n.parents("body").length&&H("body").trigger(e,a),o.push(e.result)),o}function nt(e){var t=e._iDisplayStart,n=e.fnDisplayEnd(),a=e._iDisplayLength;n<=t&&(t=n-a),t-=t%a,e._iDisplayStart=t=-1===a||t<0?0:t}function at(e,t){var e=e.renderer,n=V.ext.renderer[t];return H.isPlainObject(e)&&e[t]?n[e[t]]||n._:"string"==typeof e&&n[e]||n._}function J(e){return e.oFeatures.bServerSide?"ssp":e.ajax?"ajax":"dom"}function rt(e,t,n){var a=e.fnFormatNumber,r=e._iDisplayStart+1,o=e._iDisplayLength,i=e.fnRecordsDisplay(),l=e.fnRecordsTotal(),s=-1===o;return t.replace(/_START_/g,a.call(e,r)).replace(/_END_/g,a.call(e,e.fnDisplayEnd())).replace(/_MAX_/g,a.call(e,l)).replace(/_TOTAL_/g,a.call(e,i)).replace(/_PAGE_/g,a.call(e,s?1:Math.ceil(r/o))).replace(/_PAGES_/g,a.call(e,s?1:Math.ceil(i/o))).replace(/_ENTRIES_/g,e.api.i18n("entries","",n)).replace(/_ENTRIES-MAX_/g,e.api.i18n("entries","",l)).replace(/_ENTRIES-TOTAL_/g,e.api.i18n("entries","",i))}var ot=[],n=Array.prototype;X=function(e,t){if(!(this instanceof X))return new X(e,t);function n(e){e=e,t=V.settings,a=m(t,"nTable");var n,t,a,r=e?e.nTable&&e.oFeatures?[e]:e.nodeName&&"table"===e.nodeName.toLowerCase()?-1!==(r=a.indexOf(e))?[t[r]]:null:e&&"function"==typeof e.settings?e.settings().toArray():("string"==typeof e?n=H(e).get():e instanceof H&&(n=e.get()),n?t.filter(function(e,t){return n.includes(a[t])}):void 0):[];r&&o.push.apply(o,r)}var o=[];if(Array.isArray(e))for(var a=0,r=e.length;a<r;a++)n(e[a]);else n(e);this.context=1<o.length?x(o):o,t&&this.push.apply(this,t),this.selector={rows:null,cols:null,opts:null},X.extend(this,this,ot)},V.Api=X,H.extend(X.prototype,{any:function(){return 0!==this.count()},context:[],count:function(){return this.flatten().length},each:function(e){for(var t=0,n=this.length;t<n;t++)e.call(this,this[t],t,this);return this},eq:function(e){var t=this.context;return t.length>e?new X(t[e],this[e]):null},filter:function(e){e=n.filter.call(this,e,this);return new X(this.context,e)},flatten:function(){var e=[];return new X(this.context,e.concat.apply(e,this.toArray()))},get:function(e){return this[e]},join:n.join,includes:function(e){return-1!==this.indexOf(e)},indexOf:n.indexOf,iterator:function(e,t,n,a){var r,o,i,l,s,u,c,d,f=[],h=this.context,p=this.selector;for("string"==typeof e&&(a=n,n=t,t=e,e=!1),o=0,i=h.length;o<i;o++){var g=new X(h[o]);if("table"===t)void 0!==(r=n.call(g,h[o],o))&&f.push(r);else if("columns"===t||"rows"===t)void 0!==(r=n.call(g,h[o],this[o],o))&&f.push(r);else if("every"===t||"column"===t||"column-rows"===t||"row"===t||"cell"===t)for(c=this[o],"column-rows"===t&&(u=vt(h[o],p.opts)),l=0,s=c.length;l<s;l++)d=c[l],void 0!==(r="cell"===t?n.call(g,h[o],d.row,d.column,o,l):n.call(g,h[o],d,o,l,u))&&f.push(r)}return f.length||a?((e=(a=new X(h,e?f.concat.apply([],f):f)).selector).rows=p.rows,e.cols=p.cols,e.opts=p.opts,a):this},lastIndexOf:n.lastIndexOf,length:0,map:function(e){e=n.map.call(this,e,this);return new X(this.context,e)},pluck:function(e){var t=V.util.get(e);return this.map(function(e){return t(e)})},pop:n.pop,push:n.push,reduce:n.reduce,reduceRight:n.reduceRight,reverse:n.reverse,selector:null,shift:n.shift,slice:function(){return new X(this.context,this)},sort:n.sort,splice:n.splice,toArray:function(){return n.slice.call(this)},to$:function(){return H(this)},toJQuery:function(){return H(this)},unique:function(){return new X(this.context,x(this.toArray()))},unshift:n.unshift}),W.__apiStruct=ot,X.extend=function(e,t,n){if(n.length&&t&&(t instanceof X||t.__dt_wrapper))for(var a,r=0,o=n.length;r<o;r++)"__proto__"!==(a=n[r]).name&&(t[a.name]="function"===a.type?function(t,n,a){return function(){var e=n.apply(t||this,arguments);return X.extend(e,e,a.methodExt),e}}(e,a.val,a):"object"===a.type?{}:a.val,t[a.name].__dt_wrapper=!0,X.extend(e,t[a.name],a.propExt))},X.register=t=function(e,t){if(Array.isArray(e))for(var n=0,a=e.length;n<a;n++)X.register(e[n],t);else for(var r=e.split("."),o=ot,i=0,l=r.length;i<l;i++){var s,u,c=function(e,t){for(var n=0,a=e.length;n<a;n++)if(e[n].name===t)return e[n];return null}(o,u=(s=-1!==r[i].indexOf("()"))?r[i].replace("()",""):r[i]);c||o.push(c={name:u,val:{},methodExt:[],propExt:[],type:"object"}),i===l-1?(c.val=t,c.type="function"==typeof t?"function":H.isPlainObject(t)?"object":"other"):o=s?c.methodExt:c.propExt}},X.registerPlural=e=function(e,t,n){X.register(e,n),X.register(t,function(){var e=n.apply(this,arguments);return e===this?this:e instanceof X?e.length?Array.isArray(e[0])?new X(e.context,e[0]):e[0]:void 0:e})};function it(e,t){var n,a;return Array.isArray(e)?(n=[],e.forEach(function(e){e=it(e,t);n.push.apply(n,e)}),n.filter(function(e){return e})):"number"==typeof e?[t[e]]:(a=t.map(function(e){return e.nTable}),H(a).filter(e).map(function(){var e=a.indexOf(this);return t[e]}).toArray())}function lt(r,o,e){var t,n;e&&(t=new X(r)).one("draw",function(){e(t.ajax.json())}),"ssp"==J(r)?d(r,o):(w(r,!0),(n=r.jqXHR)&&4!==n.readyState&&n.abort(),Fe(r,{},function(e){me(r);for(var t=Ne(r,e),n=0,a=t.length;n<a;n++)D(r,t[n]);d(r,o),He(r),w(r,!1)}))}function st(e,t,n,a,r){for(var o,i,l,s,u=[],c=typeof t,d=0,f=(t=t&&"string"!=c&&"function"!=c&&void 0!==t.length?t:[t]).length;d<f;d++)for(l=0,s=(i=t[d]&&t[d].split&&!t[d].match(/[[(:]/)?t[d].split(","):[t[d]]).length;l<s;l++)(o=(o=n("string"==typeof i[l]?i[l].trim():i[l])).filter(function(e){return null!=e}))&&o.length&&(u=u.concat(o));var h=C.selector[e];if(h.length)for(d=0,f=h.length;d<f;d++)u=h[d](a,r,u);return x(u)}function ut(e){return(e=e||{}).filter&&void 0===e.search&&(e.search=e.filter),H.extend({search:"none",order:"current",page:"all"},e)}function ct(e){var t=new X(e.context[0]);return e.length&&t.push(e[0]),t.selector=e.selector,t.length&&1<t[0].length&&t[0].splice(1),t}t("tables()",function(e){return null!=e?new X(it(e,this.context)):this}),t("table()",function(e){var e=this.tables(e),t=e.context;return t.length?new X(t[0]):e}),[["nodes","node","nTable"],["body","body","nTBody"],["header","header","nTHead"],["footer","footer","nTFoot"]].forEach(function(t){e("tables()."+t[0]+"()","table()."+t[1]+"()",function(){return this.iterator("table",function(e){return e[t[2]]},1)})}),[["header","aoHeader"],["footer","aoFooter"]].forEach(function(n){t("table()."+n[0]+".structure()",function(e){var e=this.columns(e).indexes().flatten(),t=this.context[0];return Te(t,t[n[1]],e)})}),e("tables().containers()","table().container()",function(){return this.iterator("table",function(e){return e.nTableWrapper},1)}),t("tables().every()",function(n){var a=this;return this.iterator("table",function(e,t){n.call(a.table(t),t)})}),t("caption()",function(r,o){var e,t=this.context;return void 0===r?(e=t[0].captionNode)&&t.length?e.innerHTML:null:this.iterator("table",function(e){var t=H(e.nTable),n=H(e.captionNode),a=H(e.nTableWrapper);n.length||(n=H("<caption/>").html(r),e.captionNode=n[0],o)||(t.prepend(n),o=n.css("caption-side")),n.html(r),o&&(n.css("caption-side",o),n[0]._captionSide=o),(a.find("div.dataTables_scroll").length?(e="top"===o?"Head":"Foot",a.find("div.dataTables_scroll"+e+" table")):t).prepend(n)},1)}),t("caption.node()",function(){var e=this.context;return e.length?e[0].captionNode:null}),t("draw()",function(t){return this.iterator("table",function(e){"page"===t?S(e):d(e,!1===(t="string"==typeof t?"full-hold"!==t:t))})}),t("page()",function(t){return void 0===t?this.page.info().page:this.iterator("table",function(e){Xe(e,t)})}),t("page.info()",function(){var e,t,n,a,r;if(0!==this.context.length)return t=(e=this.context[0])._iDisplayStart,n=e.oFeatures.bPaginate?e._iDisplayLength:-1,a=e.fnRecordsDisplay(),{page:(r=-1===n)?0:Math.floor(t/n),pages:r?1:Math.ceil(a/n),start:t,end:e.fnDisplayEnd(),length:n,recordsTotal:e.fnRecordsTotal(),recordsDisplay:a,serverSide:"ssp"===J(e)}}),t("page.len()",function(t){return void 0===t?0!==this.context.length?this.context[0]._iDisplayLength:void 0:this.iterator("table",function(e){We(e,t)})}),t("ajax.json()",function(){var e=this.context;if(0<e.length)return e[0].json}),t("ajax.params()",function(){var e=this.context;if(0<e.length)return e[0].oAjaxData}),t("ajax.reload()",function(t,n){return this.iterator("table",function(e){lt(e,!1===n,t)})}),t("ajax.url()",function(t){var e=this.context;return void 0===t?0===e.length?void 0:(e=e[0],H.isPlainObject(e.ajax)?e.ajax.url:e.ajax):this.iterator("table",function(e){H.isPlainObject(e.ajax)?e.ajax.url=t:e.ajax=t})}),t("ajax.url().load()",function(t,n){return this.iterator("table",function(e){lt(e,!1===n,t)})});function dt(o,i,e,t){function l(e,t){var n;if(Array.isArray(e)||e instanceof H)for(var a=0,r=e.length;a<r;a++)l(e[a],t);else e.nodeName&&"tr"===e.nodeName.toLowerCase()?(e.setAttribute("data-dt-row",i.idx),s.push(e)):(n=H("<tr><td></td></tr>").attr("data-dt-row",i.idx).addClass(t),H("td",n).addClass(t).html(e)[0].colSpan=oe(o),s.push(n[0]))}var s=[];l(e,t),i._details&&i._details.detach(),i._details=H(s),i._detailsShow&&i._details.insertAfter(i.nTr)}function ft(e,t){var n=e.context;if(n.length&&e.length){var a=n[0].aoData[e[0]];if(a._details){(a._detailsShow=t)?(a._details.insertAfter(a.nTr),H(a.nTr).addClass("dt-hasChild")):(a._details.detach(),H(a.nTr).removeClass("dt-hasChild")),G(n[0],null,"childRow",[t,e.row(e[0])]);var i=n[0],r=new X(i),a=".dt.DT_details",t="draw"+a,e="column-sizing"+a,a="destroy"+a,l=i.aoData;if(r.off(t+" "+e+" "+a),m(l,"_details").length>0){r.on(t,function(e,t){if(i!==t)return;r.rows({page:"current"}).eq(0).each(function(e){var t=l[e];if(t._detailsShow)t._details.insertAfter(t.nTr)})});r.on(e,function(e,t){if(i!==t)return;var n,a=oe(t);for(var r=0,o=l.length;r<o;r++){n=l[r];if(n&&n._details)n._details.each(function(){var e=H(this).children("td");if(e.length==1)e.attr("colspan",a)})}});r.on(a,function(e,t){if(i!==t)return;for(var n=0,a=l.length;n<a;n++)if(l[n]&&l[n]._details)yt(r,n)})}bt(n)}}}function ht(e,t,n,a,r,o){for(var i=[],l=0,s=r.length;l<s;l++)i.push(B(e,r[l],t,o));return i}function pt(e,t,n){var a=e.aoHeader;return a[void 0!==n?n:e.bSortCellsTop?0:a.length-1][t].cell}function gt(t,n){return function(e){return T(e)||"string"!=typeof e||(e=e.replace(F," "),t&&(e=L(e)),n&&(e=k(e,!1))),e}}var vt=function(e,t){var n,a=[],r=e.aiDisplay,o=e.aiDisplayMaster,i=t.search,l=t.order,t=t.page;if("ssp"==J(e))return"removed"===i?[]:h(0,o.length);if("current"==t)for(u=e._iDisplayStart,c=e.fnDisplayEnd();u<c;u++)a.push(r[u]);else if("current"==l||"applied"==l){if("none"==i)a=o.slice();else if("applied"==i)a=r.slice();else if("removed"==i){for(var s={},u=0,c=r.length;u<c;u++)s[r[u]]=null;o.forEach(function(e){Object.prototype.hasOwnProperty.call(s,e)||a.push(e)})}}else if("index"==l||"original"==l)for(u=0,c=e.aoData.length;u<c;u++)e.aoData[u]&&("none"==i||-1===(n=r.indexOf(u))&&"removed"==i||0<=n&&"applied"==i)&&a.push(u);else if("number"==typeof l){var d=Je(e,l,"asc");if("none"===i)a=d;else for(u=0;u<d.length;u++)(-1===(n=r.indexOf(d[u]))&&"removed"==i||0<=n&&"applied"==i)&&a.push(d[u])}return a},mt=(t("rows()",function(n,a){void 0===n?n="":H.isPlainObject(n)&&(a=n,n=""),a=ut(a);var e=this.iterator("table",function(e){return t=st("row",t=n,function(n){var e=f(n),a=r.aoData;if(null!==e&&!o)return[e];if(i=i||vt(r,o),null!==e&&-1!==i.indexOf(e))return[e];if(null==n||""===n)return i;if("function"==typeof n)return i.map(function(e){var t=a[e];return n(e,t._aData,t.nTr)?e:null});if(n.nodeName)return e=n._DT_RowIndex,t=n._DT_CellIndex,void 0!==e?a[e]&&a[e].nTr===n?[e]:[]:t?a[t.row]&&a[t.row].nTr===n.parentNode?[t.row]:[]:(e=H(n).closest("*[data-dt-row]")).length?[e.data("dt-row")]:[];if("string"==typeof n&&"#"===n.charAt(0)){var t=r.aIds[n.replace(/^#/,"")];if(void 0!==t)return[t.idx]}e=A(b(r.aoData,i,"nTr"));return H(e).filter(n).map(function(){return this._DT_RowIndex}).toArray()},r=e,o=a),"current"!==o.order&&"applied"!==o.order||ze(r,t),t;var r,t,o,i},1);return e.selector.rows=n,e.selector.opts=a,e}),t("rows().nodes()",function(){return this.iterator("row",function(e,t){return e.aoData[t].nTr||void 0},1)}),t("rows().data()",function(){return this.iterator(!0,"rows",function(e,t){return b(e.aoData,t,"_aData")},1)}),e("rows().cache()","row().cache()",function(n){return this.iterator("row",function(e,t){e=e.aoData[t];return"search"===n?e._aFilterData:e._aSortData},1)}),e("rows().invalidate()","row().invalidate()",function(n){return this.iterator("row",function(e,t){be(e,t,n)})}),e("rows().indexes()","row().index()",function(){return this.iterator("row",function(e,t){return t},1)}),e("rows().ids()","row().id()",function(e){for(var t=[],n=this.context,a=0,r=n.length;a<r;a++)for(var o=0,i=this[a].length;o<i;o++){var l=n[a].rowIdFn(n[a].aoData[this[a][o]]._aData);t.push((!0===e?"#":"")+l)}return new X(n,t)}),e("rows().remove()","row().remove()",function(){return this.iterator("row",function(e,t){var n=e.aoData,a=n[t],r=e.aiDisplayMaster.indexOf(t),r=(-1!==r&&e.aiDisplayMaster.splice(r,1),0<e._iRecordsDisplay&&e._iRecordsDisplay--,nt(e),e.rowIdFn(a._aData));void 0!==r&&delete e.aIds[r],n[t]=null}),this}),t("rows.add()",function(o){var e=this.iterator("table",function(e){for(var t,n=[],a=0,r=o.length;a<r;a++)(t=o[a]).nodeName&&"TR"===t.nodeName.toUpperCase()?n.push(fe(e,t)[0]):n.push(D(e,t));return n},1),t=this.rows(-1);return t.pop(),t.push.apply(t,e),t}),t("row()",function(e,t){return ct(this.rows(e,t))}),t("row().data()",function(e){var t,n=this.context;return void 0===e?n.length&&this.length&&this[0].length?n[0].aoData[this[0]]._aData:void 0:((t=n[0].aoData[this[0]])._aData=e,Array.isArray(e)&&t.nTr&&t.nTr.id&&v(n[0].rowId)(e,t.nTr.id),be(n[0],this[0],"data"),this)}),t("row().node()",function(){var e=this.context;if(e.length&&this.length&&this[0].length){e=e[0].aoData[this[0]];if(e&&e.nTr)return e.nTr}return null}),t("row.add()",function(t){t instanceof H&&t.length&&(t=t[0]);var e=this.iterator("table",function(e){return t.nodeName&&"TR"===t.nodeName.toUpperCase()?fe(e,t)[0]:D(e,t)});return this.row(e[0])}),H(_).on("plugin-init.dt",function(e,t){var a=new X(t);a.on("stateSaveParams.DT",function(e,t,n){for(var a=t.rowIdFn,r=t.aiDisplayMaster,o=[],i=0;i<r.length;i++){var l=r[i],l=t.aoData[l];l._detailsShow&&o.push("#"+a(l._aData))}n.childRows=o}),a.on("stateLoaded.DT",function(e,t,n){mt(a,n)}),mt(a,a.state.loaded())}),function(e,t){t&&t.childRows&&e.rows(t.childRows.map(function(e){return e.replace(/([^:\\]*(?:\\.[^:\\]*)*):/g,"$1\\:")})).every(function(){G(e.settings()[0],null,"requestChild",[this])})}),bt=V.util.throttle(function(e){Qe(e[0])},500),yt=function(e,t){var n=e.context;n.length&&(t=n[0].aoData[void 0!==t?t:e[0]])&&t._details&&(t._details.remove(),t._detailsShow=void 0,t._details=void 0,H(t.nTr).removeClass("dt-hasChild"),bt(n))},Dt="row().child",xt=Dt+"()",St=(t(xt,function(e,t){var n=this.context;return void 0===e?n.length&&this.length&&n[0].aoData[this[0]]?n[0].aoData[this[0]]._details:void 0:(!0===e?this.child.show():!1===e?yt(this):n.length&&this.length&&dt(n[0],n[0].aoData[this[0]],e,t),this)}),t([Dt+".show()",xt+".show()"],function(){return ft(this,!0),this}),t([Dt+".hide()",xt+".hide()"],function(){return ft(this,!1),this}),t([Dt+".remove()",xt+".remove()"],function(){return yt(this),this}),t(Dt+".isShown()",function(){var e=this.context;return e.length&&this.length&&e[0].aoData[this[0]]&&e[0].aoData[this[0]]._detailsShow||!1}),/^([^:]+)?:(name|title|visIdx|visible)$/),xt=(t("columns()",function(n,a){void 0===n?n="":H.isPlainObject(n)&&(a=n,n=""),a=ut(a);var e=this.iterator("table",function(e){return t=n,l=a,s=(i=e).aoColumns,u=m(s,"sName"),c=m(s,"sTitle"),e=V.util.get("[].[].cell")(i.aoHeader),d=x(M([],e)),st("column",t,function(n){var a,e=f(n);if(""===n)return h(s.length);if(null!==e)return[0<=e?e:s.length+e];if("function"==typeof n)return a=vt(i,l),s.map(function(e,t){return n(t,ht(i,t,0,0,a),pt(i,t))?t:null});var t,r,o="string"==typeof n?n.match(St):"";if(o)switch(o[2]){case"visIdx":case"visible":return o[1]?(t=parseInt(o[1],10))<0?[(r=s.map(function(e,t){return e.bVisible?t:null}))[r.length+t]]:[ae(i,t)]:s.map(function(e,t){return e.bVisible?t:null});case"name":return u.map(function(e,t){return e===o[1]?t:null});case"title":return c.map(function(e,t){return e===o[1]?t:null});default:return[]}return n.nodeName&&n._DT_CellIndex?[n._DT_CellIndex.column]:(e=H(d).filter(n).map(function(){return de(this)}).toArray()).length||!n.nodeName?e:(e=H(n).closest("*[data-dt-column]")).length?[e.data("dt-column")]:[]},i,l);var i,t,l,s,u,c,d},1);return e.selector.cols=n,e.selector.opts=a,e}),e("columns().header()","column().header()",function(n){return this.iterator("column",function(e,t){return pt(e,t,n)},1)}),e("columns().footer()","column().footer()",function(n){return this.iterator("column",function(e,t){return e.aoFooter.length?e.aoFooter[void 0!==n?n:0][t].cell:null},1)}),e("columns().data()","column().data()",function(){return this.iterator("column-rows",ht,1)}),e("columns().render()","column().render()",function(o){return this.iterator("column-rows",function(e,t,n,a,r){return ht(e,t,0,0,r,o)},1)}),e("columns().dataSrc()","column().dataSrc()",function(){return this.iterator("column",function(e,t){return e.aoColumns[t].mData},1)}),e("columns().cache()","column().cache()",function(o){return this.iterator("column-rows",function(e,t,n,a,r){return b(e.aoData,r,"search"===o?"_aFilterData":"_aSortData",t)},1)}),e("columns().init()","column().init()",function(){return this.iterator("column",function(e,t){return e.aoColumns[t]},1)}),e("columns().nodes()","column().nodes()",function(){return this.iterator("column-rows",function(e,t,n,a,r){return b(e.aoData,r,"anCells",t)},1)}),e("columns().titles()","column().title()",function(n,a){return this.iterator("column",function(e,t){"number"==typeof n&&(a=n,n=void 0);t=H("span.dt-column-title",this.column(t).header(a));return void 0!==n?(t.html(n),this):t.html()},1)}),e("columns().types()","column().type()",function(){return this.iterator("column",function(e,t){t=e.aoColumns[t].sType;return t||se(e),t},1)}),e("columns().visible()","column().visible()",function(n,a){var t=this,r=[],e=this.iterator("column",function(e,t){if(void 0===n)return e.aoColumns[t].bVisible;!function(e,t,n){var a,r,o=e.aoColumns,i=o[t],l=e.aoData;if(void 0===n)return i.bVisible;if(i.bVisible===n)return!1;if(n)for(var s=m(o,"bVisible").indexOf(!0,t+1),u=0,c=l.length;u<c;u++)l[u]&&(r=l[u].nTr,a=l[u].anCells,r)&&r.insertBefore(a[t],a[s]||null);else H(m(e.aoData,"anCells",t)).detach();return i.bVisible=n,Ue(e),!0}(e,t,n)||r.push(t)});return void 0!==n&&this.iterator("table",function(e){_e(e,e.aoHeader),_e(e,e.aoFooter),e.aiDisplay.length||H(e.nTBody).find("td[colspan]").attr("colspan",oe(e)),Qe(e),t.iterator("column",function(e,t){r.includes(t)&&G(e,null,"column-visibility",[e,t,n,a])}),r.length&&(void 0===a||a)&&t.columns.adjust()}),e}),e("columns().widths()","column().width()",function(){var e=this.columns(":visible").count(),e=H("<tr>").html("<td>"+Array(e).join("</td><td>")+"</td>"),n=(H(this.table().body()).append(e),e.children().map(function(){return H(this).outerWidth()}));return e.remove(),this.iterator("column",function(e,t){e=re(e,t);return null!==e?n[e]:0},1)}),e("columns().indexes()","column().index()",function(n){return this.iterator("column",function(e,t){return"visible"===n?re(e,t):t},1)}),t("columns.adjust()",function(){return this.iterator("table",function(e){ne(e)},1)}),t("column.index()",function(e,t){var n;if(0!==this.context.length)return n=this.context[0],"fromVisible"===e||"toData"===e?ae(n,t):"fromData"===e||"toVisible"===e?re(n,t):void 0}),t("column()",function(e,t){return ct(this.columns(e,t))}),t("cells()",function(g,e,v){var a,r,o,i,l,s,t;return H.isPlainObject(g)&&(void 0===g.row?(v=g,g=null):(v=e,e=null)),H.isPlainObject(e)&&(v=e,e=null),null==e?this.iterator("table",function(e){return a=e,e=g,t=ut(v),d=a.aoData,f=vt(a,t),n=A(b(d,f,"anCells")),h=H(M([],n)),p=a.aoColumns.length,st("cell",e,function(e){var t,n="function"==typeof e;if(null==e||n){for(o=[],i=0,l=f.length;i<l;i++)for(r=f[i],s=0;s<p;s++)u={row:r,column:s},(!n||(c=d[r],e(u,B(a,r,s),c.anCells?c.anCells[s]:null)))&&o.push(u);return o}return H.isPlainObject(e)?void 0!==e.column&&void 0!==e.row&&-1!==f.indexOf(e.row)?[e]:[]:(t=h.filter(e).map(function(e,t){return{row:t._DT_CellIndex.row,column:t._DT_CellIndex.column}}).toArray()).length||!e.nodeName?t:(c=H(e).closest("*[data-dt-row]")).length?[{row:c.data("dt-row"),column:c.data("dt-column")}]:[]},a,t);var a,t,r,o,i,l,s,u,c,d,f,n,h,p}):(t=v?{page:v.page,order:v.order,search:v.search}:{},a=this.columns(e,t),r=this.rows(g,t),t=this.iterator("table",function(e,t){var n=[];for(o=0,i=r[t].length;o<i;o++)for(l=0,s=a[t].length;l<s;l++)n.push({row:r[t][o],column:a[t][l]});return n},1),t=v&&v.selected?this.cells(t,v):t,H.extend(t.selector,{cols:e,rows:g,opts:v}),t)}),e("cells().nodes()","cell().node()",function(){return this.iterator("cell",function(e,t,n){e=e.aoData[t];return e&&e.anCells?e.anCells[n]:void 0},1)}),t("cells().data()",function(){return this.iterator("cell",function(e,t,n){return B(e,t,n)},1)}),e("cells().cache()","cell().cache()",function(a){return a="search"===a?"_aFilterData":"_aSortData",this.iterator("cell",function(e,t,n){return e.aoData[t][a][n]},1)}),e("cells().render()","cell().render()",function(a){return this.iterator("cell",function(e,t,n){return B(e,t,n,a)},1)}),e("cells().indexes()","cell().index()",function(){return this.iterator("cell",function(e,t,n){return{row:t,column:n,columnVisible:re(e,n)}},1)}),e("cells().invalidate()","cell().invalidate()",function(a){return this.iterator("cell",function(e,t,n){be(e,t,a,n)})}),t("cell()",function(e,t,n){return ct(this.cells(e,t,n))}),t("cell().data()",function(e){var t,n,a,r,o,i=this.context,l=this[0];return void 0===e?i.length&&l.length?B(i[0],l[0].row,l[0].column):void 0:(t=i[0],n=l[0].row,a=l[0].column,r=t.aoColumns[a],o=t.aoData[n]._aData,r.fnSetData(o,e,{settings:t,row:n,col:a}),be(i[0],l[0].row,"data",l[0].column),this)}),t("order()",function(t,e){var n=this.context,a=Array.prototype.slice.call(arguments);return void 0===t?0!==n.length?n[0].aaSorting:void 0:("number"==typeof t?t=[[t,e]]:1<a.length&&(t=a),this.iterator("table",function(e){e.aaSorting=Array.isArray(t)?t.slice():t}))}),t("order.listener()",function(t,n,a){return this.iterator("table",function(e){$e(e,t,{},n,a)})}),t("order.fixed()",function(t){var e;return t?this.iterator("table",function(e){e.aaSortingFixed=H.extend(!0,{},t)}):(e=(e=this.context).length?e[0].aaSortingFixed:void 0,Array.isArray(e)?{pre:e}:e)}),t(["columns().order()","column().order()"],function(n){var a=this;return n?this.iterator("table",function(e,t){e.aaSorting=a[t].map(function(e){return[e,n]})}):this.iterator("column",function(e,t){for(var n=Ge(e),a=0,r=n.length;a<r;a++)if(n[a].col===t)return n[a].dir;return null},1)}),e("columns().orderable()","column().orderable()",function(n){return this.iterator("column",function(e,t){e=e.aoColumns[t];return n?e.asSorting:e.bSortable},1)}),t("processing()",function(t){return this.iterator("table",function(e){w(e,t)})}),t("search()",function(t,n,a,r){var e=this.context;return void 0===t?0!==e.length?e[0].oPreviousSearch.search:void 0:this.iterator("table",function(e){e.oFeatures.bFilter&&Re(e,"object"==typeof n?H.extend(e.oPreviousSearch,n,{search:t}):H.extend(e.oPreviousSearch,{search:t,regex:null!==n&&n,smart:null===a||a,caseInsensitive:null===r||r}))})}),t("search.fixed()",function(t,n){var e=this.iterator(!0,"table",function(e){e=e.searchFixed;return t?void 0===n?e[t]:(null===n?delete e[t]:e[t]=n,this):Object.keys(e)});return void 0!==t&&void 0===n?e[0]:e}),e("columns().search()","column().search()",function(a,r,o,i){return this.iterator("column",function(e,t){var n=e.aoPreSearchCols;if(void 0===a)return n[t].search;e.oFeatures.bFilter&&("object"==typeof r?H.extend(n[t],r,{search:a}):H.extend(n[t],{search:a,regex:null!==r&&r,smart:null===o||o,caseInsensitive:null===i||i}),Re(e,e.oPreviousSearch))})}),t(["columns().search.fixed()","column().search.fixed()"],function(n,a){var e=this.iterator(!0,"column",function(e,t){e=e.aoColumns[t].searchFixed;return n?void 0===a?e[n]:(null===a?delete e[n]:e[n]=a,this):Object.keys(e)});return void 0!==n&&void 0===a?e[0]:e}),t("state()",function(e,t){var n;return e?(n=H.extend(!0,{},e),this.iterator("table",function(e){!1!==t&&(n.time=+new Date+100),Ke(e,n,function(){})})):this.context.length?this.context[0].oSavedState:null}),t("state.clear()",function(){return this.iterator("table",function(e){e.fnStateSaveCallback.call(e.oInstance,e,{})})}),t("state.loaded()",function(){return this.context.length?this.context[0].oLoadedState:null}),t("state.save()",function(){return this.iterator("table",function(e){Qe(e)})}),V.use=function(e,t){var n="string"==typeof e?t:e,t="string"==typeof t?t:e;if(void 0===n&&"string"==typeof t)switch(t){case"lib":case"jq":return H;case"win":return W;case"datetime":return V.DateTime;case"luxon":return o;case"moment":return i;default:return null}"lib"===t||"jq"===t||n&&n.fn&&n.fn.jquery?H=n:"win"==t||n&&n.document?_=(W=n).document:"datetime"===t||n&&"DateTime"===n.type?V.DateTime=n:"luxon"===t||n&&n.FixedOffsetZone?o=n:("moment"===t||n&&n.isMoment)&&(i=n)},V.factory=function(e,t){var n=!1;return e&&e.document&&(_=(W=e).document),t&&t.fn&&t.fn.jquery&&(H=t,n=!0),n},V.versionCheck=function(e,t){for(var n,a,r=(t||V.version).split("."),o=e.split("."),i=0,l=o.length;i<l;i++)if((n=parseInt(r[i],10)||0)!==(a=parseInt(o[i],10)||0))return a<n;return!0},V.isDataTable=function(e){var r=H(e).get(0),o=!1;return e instanceof V.Api||(H.each(V.settings,function(e,t){var n=t.nScrollHead?H("table",t.nScrollHead)[0]:null,a=t.nScrollFoot?H("table",t.nScrollFoot)[0]:null;t.nTable!==r&&n!==r&&a!==r||(o=!0)}),o)},V.tables=function(t){var e=!1,n=(H.isPlainObject(t)&&(e=t.api,t=t.visible),V.settings.filter(function(e){return!(t&&!H(e.nTable).is(":visible"))}).map(function(e){return e.nTable}));return e?new X(n):n},V.camelToHungarian=q,t("$()",function(e,t){t=this.rows(t).nodes(),t=H(t);return H([].concat(t.filter(e).toArray(),t.find(e).toArray()))}),H.each(["on","one","off"],function(e,n){t(n+"()",function(){var e=Array.prototype.slice.call(arguments),t=(e[0]=e[0].split(/\s/).map(function(e){return e.match(/\.dt\b/)?e:e+".dt"}).join(" "),H(this.tables().nodes()));return t[n].apply(t,e),this})}),t("clear()",function(){return this.iterator("table",function(e){me(e)})}),t("error()",function(t){return this.iterator("table",function(e){$(e,0,t)})}),t("settings()",function(){return new X(this.context,this.context)}),t("init()",function(){var e=this.context;return e.length?e[0].oInit:null}),t("data()",function(){return this.iterator("table",function(e){return m(e.aoData,"_aData")}).flatten()}),t("trigger()",function(t,n,a){return this.iterator("table",function(e){return G(e,null,t,n,a)}).flatten()}),t("ready()",function(e){var t=this.context;return e?this.tables().every(function(){this.context[0]._bInitComplete?e.call(this):this.on("init",function(){e.call(this)})}):t.length?t[0]._bInitComplete||!1:null}),t("destroy()",function(c){return c=c||!1,this.iterator("table",function(e){var t=e.oClasses,n=e.nTable,a=e.nTBody,r=e.nTHead,o=e.nTFoot,i=H(n),a=H(a),l=H(e.nTableWrapper),s=e.aoData.map(function(e){return e?e.nTr:null}),u=t.order,o=(e.bDestroying=!0,G(e,"aoDestroyCallback","destroy",[e],!0),c||new X(e).columns().visible(!0),l.off(".DT").find(":not(tbody *)").off(".DT"),H(W).off(".DT-"+e.sInstance),n!=r.parentNode&&(i.children("thead").detach(),i.append(r)),o&&n!=o.parentNode&&(i.children("tfoot").detach(),i.append(o)),e.colgroup.remove(),e.aaSorting=[],e.aaSortingFixed=[],Ze(e),H("th, td",r).removeClass(u.canAsc+" "+u.canDesc+" "+u.isAsc+" "+u.isDesc).css("width",""),a.children().detach(),a.append(s),e.nTableWrapper.parentNode),r=e.nTableWrapper.nextSibling,u=c?"remove":"detach",a=(i[u](),l[u](),!c&&o&&(o.insertBefore(n,r),i.css("width",e.sDestroyWidth).removeClass(t.table)),V.settings.indexOf(e));-1!==a&&V.settings.splice(a,1)})}),H.each(["column","row","cell"],function(e,s){t(s+"s().every()",function(a){var r,o=this.selector.opts,i=this,l=0;return this.iterator("every",function(e,t,n){r=i[s](t,o),"cell"===s?a.call(r,r[0][0].row,r[0][0].column,n,l):a.call(r,t,n,l),l++})})}),t("i18n()",function(e,t,n){var a=this.context[0],e=U(e)(a.oLanguage);return"string"==typeof(e=H.isPlainObject(e=void 0===e?t:e)?void 0!==n&&void 0!==e[n]?e[n]:e._:e)?e.replace("%d",n):e}),V.version="2.1.3",V.settings=[],V.models={},V.models.oSearch={caseInsensitive:!0,search:"",regex:!1,smart:!0,return:!1},V.models.oRow={nTr:null,anCells:null,_aData:[],_aSortData:null,_aFilterData:null,_sFilterRow:null,src:null,idx:-1,displayData:null},V.models.oColumn={idx:null,aDataSort:null,asSorting:null,bSearchable:null,bSortable:null,bVisible:null,_sManualType:null,_bAttrSrc:!1,fnCreatedCell:null,fnGetData:null,fnSetData:null,mData:null,mRender:null,sClass:null,sContentPadding:null,sDefaultContent:null,sName:null,sSortDataType:"std",sSortingClass:null,sTitle:null,sType:null,sWidth:null,sWidthOrig:null,maxLenString:null,searchFixed:null},V.defaults={aaData:null,aaSorting:[[0,"asc"]],aaSortingFixed:[],ajax:null,aLengthMenu:[10,25,50,100],aoColumns:null,aoColumnDefs:null,aoSearchCols:[],bAutoWidth:!0,bDeferRender:!0,bDestroy:!1,bFilter:!0,bInfo:!0,bLengthChange:!0,bPaginate:!0,bProcessing:!1,bRetrieve:!1,bScrollCollapse:!1,bServerSide:!1,bSort:!0,bSortMulti:!0,bSortCellsTop:null,bSortClasses:!0,bStateSave:!1,fnCreatedRow:null,fnDrawCallback:null,fnFooterCallback:null,fnFormatNumber:function(e){return e.toString().replace(/\B(?=(\d{3})+(?!\d))/g,this.oLanguage.sThousands)},fnHeaderCallback:null,fnInfoCallback:null,fnInitComplete:null,fnPreDrawCallback:null,fnRowCallback:null,fnStateLoadCallback:function(e){try{return JSON.parse((-1===e.iStateDuration?sessionStorage:localStorage).getItem("DataTables_"+e.sInstance+"_"+location.pathname))}catch(e){return{}}},fnStateLoadParams:null,fnStateLoaded:null,fnStateSaveCallback:function(e,t){try{(-1===e.iStateDuration?sessionStorage:localStorage).setItem("DataTables_"+e.sInstance+"_"+location.pathname,JSON.stringify(t))}catch(e){}},fnStateSaveParams:null,iStateDuration:7200,iDisplayLength:10,iDisplayStart:0,iTabIndex:0,oClasses:{},oLanguage:{oAria:{orderable:": Activate to sort",orderableReverse:": Activate to invert sorting",orderableRemove:": Activate to remove sorting",paginate:{first:"First",last:"Last",next:"Next",previous:"Previous",number:""}},oPaginate:{sFirst:"«",sLast:"»",sNext:"›",sPrevious:"‹"},entries:{_:"entries",1:"entry"},sEmptyTable:"No data available in table",sInfo:"Showing _START_ to _END_ of _TOTAL_ _ENTRIES-TOTAL_",sInfoEmpty:"Showing 0 to 0 of 0 _ENTRIES-TOTAL_",sInfoFiltered:"(filtered from _MAX_ total _ENTRIES-MAX_)",sInfoPostFix:"",sDecimal:"",sThousands:",",sLengthMenu:"_MENU_ _ENTRIES_ per page",sLoadingRecords:"Loading...",sProcessing:"",sSearch:"Search:",sSearchPlaceholder:"",sUrl:"",sZeroRecords:"No matching records found"},orderDescReverse:!0,oSearch:H.extend({},V.models.oSearch),layout:{topStart:"pageLength",topEnd:"search",bottomStart:"info",bottomEnd:"paging"},sDom:null,searchDelay:null,sPaginationType:"",sScrollX:"",sScrollXInner:"",sScrollY:"",sServerMethod:"GET",renderer:null,rowId:"DT_RowId",caption:null,iDeferLoading:null},Z(V.defaults),V.defaults.column={aDataSort:null,iDataSort:-1,ariaTitle:"",asSorting:["asc","desc",""],bSearchable:!0,bSortable:!0,bVisible:!0,fnCreatedCell:null,mData:null,mRender:null,sCellType:"td",sClass:"",sContentPadding:"",sDefaultContent:null,sName:"",sSortDataType:"std",sTitle:null,sType:null,sWidth:null},Z(V.defaults.column),V.models.oSettings={oFeatures:{bAutoWidth:null,bDeferRender:null,bFilter:null,bInfo:!0,bLengthChange:!0,bPaginate:null,bProcessing:null,bServerSide:null,bSort:null,bSortMulti:null,bSortClasses:null,bStateSave:null},oScroll:{bCollapse:null,iBarWidth:0,sX:null,sXInner:null,sY:null},oLanguage:{fnInfoCallback:null},oBrowser:{bScrollbarLeft:!1,barWidth:0},ajax:null,aanFeatures:[],aoData:[],aiDisplay:[],aiDisplayMaster:[],aIds:{},aoColumns:[],aoHeader:[],aoFooter:[],oPreviousSearch:{},searchFixed:{},aoPreSearchCols:[],aaSorting:null,aaSortingFixed:[],sDestroyWidth:0,aoRowCallback:[],aoHeaderCallback:[],aoFooterCallback:[],aoDrawCallback:[],aoRowCreatedCallback:[],aoPreDrawCallback:[],aoInitComplete:[],aoStateSaveParams:[],aoStateLoadParams:[],aoStateLoaded:[],sTableId:"",nTable:null,nTHead:null,nTFoot:null,nTBody:null,nTableWrapper:null,bInitialised:!1,aoOpenRows:[],sDom:null,searchDelay:null,sPaginationType:"two_button",pagingControls:0,iStateDuration:0,aoStateSave:[],aoStateLoad:[],oSavedState:null,oLoadedState:null,bAjaxDataGet:!0,jqXHR:null,json:void 0,oAjaxData:void 0,sServerMethod:null,fnFormatNumber:null,aLengthMenu:null,iDraw:0,bDrawing:!1,iDrawError:-1,_iDisplayLength:10,_iDisplayStart:0,_iRecordsTotal:0,_iRecordsDisplay:0,oClasses:{},bFiltered:!1,bSorted:!1,bSortCellsTop:null,oInit:null,aoDestroyCallback:[],fnRecordsTotal:function(){return"ssp"==J(this)?+this._iRecordsTotal:this.aiDisplayMaster.length},fnRecordsDisplay:function(){return"ssp"==J(this)?+this._iRecordsDisplay:this.aiDisplay.length},fnDisplayEnd:function(){var e=this._iDisplayLength,t=this._iDisplayStart,n=t+e,a=this.aiDisplay.length,r=this.oFeatures,o=r.bPaginate;return r.bServerSide?!1===o||-1===e?t+a:Math.min(t+e,this._iRecordsDisplay):!o||a<n||-1===e?a:n},oInstance:null,sInstance:null,iTabIndex:0,nScrollHead:null,nScrollFoot:null,aLastSort:[],oPlugins:{},rowIdFn:null,rowId:null,caption:"",captionNode:null,colgroup:null,deferLoading:null},V.ext.pager);H.extend(xt,{simple:function(){return["previous","next"]},full:function(){return["first","previous","next","last"]},numbers:function(){return["numbers"]},simple_numbers:function(){return["previous","numbers","next"]},full_numbers:function(){return["first","previous","numbers","next","last"]},first_last:function(){return["first","last"]},first_last_numbers:function(){return["first","numbers","last"]},_numbers:Et,numbers_length:7}),H.extend(!0,V.ext.renderer,{pagingButton:{_:function(e,t,n,a,r){var e=e.oClasses.paging,o=[e.button];return a&&o.push(e.active),r&&o.push(e.disabled),{display:a="ellipsis"===t?H('<span class="ellipsis"></span>').html(n)[0]:H("<button>",{class:o.join(" "),role:"link",type:"button"}).html(n),clicker:a}}},pagingContainer:{_:function(e,t){return t}}});function wt(e,t,n,a,r){return i?e[t](r):o?e[n](r):a?e[a](r):e}var o,i,Tt=!1;function _t(e,t,n){var a;if(W.luxon&&!o&&(o=W.luxon),i=W.moment&&!i?W.moment:i){if(!(a=i.utc(e,t,n,!0)).isValid())return null}else if(o){if(!(a=t&&"string"==typeof e?o.DateTime.fromFormat(e,t):o.DateTime.fromISO(e)).isValid)return null;a.setLocale(n)}else t?(Tt||alert("DataTables warning: Formatted date without Moment.js or Luxon - https://datatables.net/tn/17"),Tt=!0):a=new Date(e);return a}function Ct(s){return function(a,r,o,i){0===arguments.length?(o="en",a=r=null):1===arguments.length?(o="en",r=a,a=null):2===arguments.length&&(o=r,r=a,a=null);var l="datetime"+(r?"-"+r:"");return V.ext.type.order[l]||V.type(l,{detect:function(e){return e===l&&l},order:{pre:function(e){return e.valueOf()}},className:"dt-right"}),function(e,t){var n;return null==e&&(e="--now"===i?(n=new Date,new Date(Date.UTC(n.getFullYear(),n.getMonth(),n.getDate(),n.getHours(),n.getMinutes(),n.getSeconds()))):""),"type"===t?l:""===e?"sort"!==t?"":_t("0000-01-01 00:00:00",null,o):!(null===r||a!==r||"sort"===t||"type"===t||e instanceof Date)||null===(n=_t(e,a,o))?e:"sort"===t?n:(e=null===r?wt(n,"toDate","toJSDate","")[s]():wt(n,"format","toFormat","toISOString",r),"display"===t?u(e):e)}}}var Lt=",",It=".";if(void 0!==W.Intl)try{for(var At=(new Intl.NumberFormat).formatToParts(100000.1),a=0;a<At.length;a++)"group"===At[a].type?Lt=At[a].value:"decimal"===At[a].type&&(It=At[a].value)}catch(e){}V.datetime=function(n,a){var r="datetime-"+n;a=a||"en",V.ext.type.order[r]||V.type(r,{detect:function(e){var t=_t(e,n,a);return!(""!==e&&!t)&&r},order:{pre:function(e){return _t(e,n,a)||0}},className:"dt-right"})},V.render={date:Ct("toLocaleDateString"),datetime:Ct("toLocaleString"),time:Ct("toLocaleTimeString"),number:function(r,o,i,l,s){return null==r&&(r=Lt),null==o&&(o=It),{display:function(e){if("number"!=typeof e&&"string"!=typeof e)return e;if(""===e||null===e)return e;var t=e<0?"-":"",n=parseFloat(e),a=Math.abs(n);if(1e11<=a||a<1e-4&&0!==a)return(a=n.toExponential(i).split(/e\+?/))[0]+" x 10<sup>"+a[1]+"</sup>";if(isNaN(n))return u(e);n=n.toFixed(i),e=Math.abs(n);a=parseInt(e,10),n=i?o+(e-a).toFixed(i).substring(2):"";return(t=0===a&&0===parseFloat(n)?"":t)+(l||"")+a.toString().replace(/\B(?=(\d{3})+(?!\d))/g,r)+n+(s||"")}}},text:function(){return{display:u,filter:u}}};function Ft(e,t){return e=e.toString().toLowerCase(),t=t.toString().toLowerCase(),e.localeCompare(t,navigator.languages[0]||navigator.language,{numeric:!0,ignorePunctuation:!0})}var l=V.ext.type,Nt=(V.type=function(n,e,t){if(!e)return{className:l.className[n],detect:l.detect.find(function(e){return e.name===n}),order:{pre:l.order[n+"-pre"],asc:l.order[n+"-asc"],desc:l.order[n+"-desc"]},render:l.render[n],search:l.search[n]};function a(e,t){l[e][n]=t}function r(e){Object.defineProperty(e,"name",{value:n});var t=l.detect.findIndex(function(e){return e.name===n});-1===t?l.detect.unshift(e):l.detect.splice(t,1,e)}function o(e){l.order[n+"-pre"]=e.pre,l.order[n+"-asc"]=e.asc,l.order[n+"-desc"]=e.desc}void 0===t&&(t=e,e=null),"className"===e?a("className",t):"detect"===e?r(t):"order"===e?o(t):"render"===e?a("render",t):"search"===e?a("search",t):e||(t.className&&a("className",t.className),void 0!==t.detect&&r(t.detect),t.order&&o(t.order),void 0!==t.render&&a("render",t.render),void 0!==t.search&&a("search",t.search))},V.types=function(){return l.detect.map(function(e){return e.name})},V.type("string",{detect:function(){return"string"},order:{pre:function(e){return T(e)?"":"string"==typeof e?e.toLowerCase():e.toString?e.toString():""}},search:gt(!1,!0)}),V.type("string-utf8",{detect:{allOf:function(e){return!0},oneOf:function(e){return!T(e)&&navigator.languages&&"string"==typeof e&&e.match(/[^\x00-\x7F]/)}},order:{asc:Ft,desc:function(e,t){return-1*Ft(e,t)}},search:gt(!1,!0)}),V.type("html",{detect:{allOf:function(e){return T(e)||"string"==typeof e&&-1!==e.indexOf("<")},oneOf:function(e){return!T(e)&&"string"==typeof e&&-1!==e.indexOf("<")}},order:{pre:function(e){return T(e)?"":e.replace?L(e).trim().toLowerCase():e+""}},search:gt(!0,!0)}),V.type("date",{className:"dt-type-date",detect:{allOf:function(e){var t;return!e||e instanceof Date||R.test(e)?null!==(t=Date.parse(e))&&!isNaN(t)||T(e):null},oneOf:function(e){return e instanceof Date||"string"==typeof e&&R.test(e)}},order:{pre:function(e){e=Date.parse(e);return isNaN(e)?-1/0:e}}}),V.type("html-num-fmt",{className:"dt-type-numeric",detect:{allOf:function(e,t){t=t.oLanguage.sDecimal;return c(e,t,!0,!1)},oneOf:function(e,t){t=t.oLanguage.sDecimal;return c(e,t,!0,!1)}},order:{pre:function(e,t){t=t.oLanguage.sDecimal;return Nt(e,t,N,P)}},search:gt(!0,!0)}),V.type("html-num",{className:"dt-type-numeric",detect:{allOf:function(e,t){t=t.oLanguage.sDecimal;return c(e,t,!1,!0)},oneOf:function(e,t){t=t.oLanguage.sDecimal;return c(e,t,!1,!1)}},order:{pre:function(e,t){t=t.oLanguage.sDecimal;return Nt(e,t,N)}},search:gt(!0,!0)}),V.type("num-fmt",{className:"dt-type-numeric",detect:{allOf:function(e,t){t=t.oLanguage.sDecimal;return s(e,t,!0,!0)},oneOf:function(e,t){t=t.oLanguage.sDecimal;return s(e,t,!0,!1)}},order:{pre:function(e,t){t=t.oLanguage.sDecimal;return Nt(e,t,P)}}}),V.type("num",{className:"dt-type-numeric",detect:{allOf:function(e,t){t=t.oLanguage.sDecimal;return s(e,t,!1,!0)},oneOf:function(e,t){t=t.oLanguage.sDecimal;return s(e,t,!1,!1)}},order:{pre:function(e,t){t=t.oLanguage.sDecimal;return Nt(e,t)}}}),function(e,t,n,a){var r;return 0===e||e&&"-"!==e?"number"==(r=typeof e)||"bigint"==r?e:+(e=(e=t?E(e,t):e).replace&&(n&&(e=e.replace(n,"")),a)?e.replace(a,""):e):-1/0});function jt(e,t,n){n&&(e[t]=n)}H.extend(!0,V.ext.renderer,{footer:{_:function(e,t,n){t.addClass(n.tfoot.cell)}},header:{_:function(p,g,v){g.addClass(v.thead.cell),p.oFeatures.bSort||g.addClass(v.order.none);var e=p.bSortCellsTop,t=g.closest("thead").find("tr"),n=g.parent().index();"disable"===g.attr("data-dt-order")||"disable"===g.parent().attr("data-dt-order")||!0===e&&0!==n||!1===e&&n!==t.length-1||H(p.nTable).on("order.dt.DT column-visibility.dt.DT",function(e,t){if(p===t){for(var n=v.order,a=t.api.columns(g),r=p.aoColumns[a.flatten()[0]],o=a.orderable().includes(!0),i="",l=a.indexes(),s=a.orderable(!0).flatten(),u=t.sortDetails,c=m(u,"col"),d=(g.removeClass(n.isAsc+" "+n.isDesc).toggleClass(n.none,!o).toggleClass(n.canAsc,o&&s.includes("asc")).toggleClass(n.canDesc,o&&s.includes("desc")),!0),f=0;f<l.length;f++)c.includes(l[f])||(d=!1);d&&(s=a.order(),g.addClass(s.includes("asc")?n.isAsc:""+s.includes("desc")?n.isDesc:""));var h=-1;for(f=0;f<c.length;f++)if(p.aoColumns[c[f]].bVisible){h=c[f];break}l[0]==h?(a=u[0],s=r.asSorting,g.attr("aria-sort","asc"===a.dir?"ascending":"descending"),i=s[a.index+1]?"Reverse":"Remove"):g.removeAttr("aria-sort"),g.attr("aria-label",o?r.ariaTitle+t.api.i18n("oAria.orderable"+i):r.ariaTitle),o&&(g.find(".dt-column-title").attr("role","button"),g.attr("tabindex",0))}})}},layout:{_:function(e,t,n){var a=e.oClasses.layout,r=H("<div/>").attr("id",n.id||null).addClass(n.className||a.row).appendTo(t);H.each(n,function(e,t){var n;"id"!==e&&"className"!==e&&(n="",t.table&&(r.addClass(a.tableRow),n+=a.tableCell+" "),n+="start"===e?a.start:"end"===e?a.end:a.full,H("<div/>").attr({id:t.id||null,class:t.className||a.cell+" "+n}).append(t.contents).appendTo(r))})}}}),V.feature={},V.feature.register=function(e,t,n){V.ext.features[e]=t,n&&C.feature.push({cFeature:n,fnInit:t})},V.feature.register("div",function(e,t){var n=H("<div>")[0];return t&&(jt(n,"className",t.className),jt(n,"id",t.id),jt(n,"innerHTML",t.html),jt(n,"textContent",t.text)),n}),V.feature.register("info",function(e,s){var t,n,u;return e.oFeatures.bInfo?(t=e.oLanguage,n=e.sTableId,u=H("<div/>",{class:e.oClasses.info.container}),s=H.extend({callback:t.fnInfoCallback,empty:t.sInfoEmpty,postfix:t.sInfoPostFix,search:t.sInfoFiltered,text:t.sInfo},s),e.aoDrawCallback.push(function(e){var t=s,n=u,a=e._iDisplayStart+1,r=e.fnDisplayEnd(),o=e.fnRecordsTotal(),i=e.fnRecordsDisplay(),l=i?t.text:t.empty;i!==o&&(l+=" "+t.search),l+=t.postfix,l=rt(e,l),t.callback&&(l=t.callback.call(e.oInstance,e,a,r,o,i,l)),n.html(l),G(e,null,"info",[e,n[0],l])}),e._infoEl||(u.attr({"aria-live":"polite",id:n+"_info",role:"status"}),H(e.nTable).attr("aria-describedby",n+"_info"),e._infoEl=u),u):null},"i");var Rt=0;function Ot(e){var t=[];return e.numbers&&t.push("numbers"),e.previousNext&&(t.unshift("previous"),t.push("next")),e.firstLast&&(t.unshift("first"),t.push("last")),t}function Pt(e,t,n,a){var r=e.oLanguage.oPaginate,o={display:"",active:!1,disabled:!1};switch(t){case"ellipsis":o.display="…",o.disabled=!0;break;case"first":o.display=r.sFirst,0===n&&(o.disabled=!0);break;case"previous":o.display=r.sPrevious,0===n&&(o.disabled=!0);break;case"next":o.display=r.sNext,0!==a&&n!==a-1||(o.disabled=!0);break;case"last":o.display=r.sLast,0!==a&&n!==a-1||(o.disabled=!0);break;default:"number"==typeof t&&(o.display=e.fnFormatNumber(t+1),n===t)&&(o.active=!0)}return o}function Et(e,t,n,a){var r=[],o=Math.floor(n/2),i=a?2:1,l=a?1:0;return t<=n?r=h(0,t):1===n?r=[e]:3===n?e<=1?r=[0,1,"ellipsis"]:t-2<=e?(r=h(t-2,t)).unshift("ellipsis"):r=["ellipsis",e,"ellipsis"]:e<=o?((r=h(0,n-i)).push("ellipsis"),a&&r.push(t-1)):t-1-o<=e?((r=h(t-(n-i),t)).unshift("ellipsis"),a&&r.unshift(0)):((r=h(e-o+i,e+o-l)).push("ellipsis"),r.unshift("ellipsis"),a&&(r.push(t-1),r.unshift(0))),r}V.feature.register("search",function(n,a){var e,t,r,o,i,l,s,u,c,d;return n.oFeatures.bFilter?(e=n.oClasses.search,t=n.sTableId,c=n.oLanguage,r=n.oPreviousSearch,o='<input type="search" class="'+e.input+'"/>',-1===(a=H.extend({placeholder:c.sSearchPlaceholder,processing:!1,text:c.sSearch},a)).text.indexOf("_INPUT_")&&(a.text+="_INPUT_"),a.text=rt(n,a.text),c=a.text.match(/_INPUT_$/),s=a.text.match(/^_INPUT_/),i=a.text.replace(/_INPUT_/,""),l="<label>"+a.text+"</label>",s?l="_INPUT_<label>"+i+"</label>":c&&(l="<label>"+i+"</label>_INPUT_"),(s=H("<div>").addClass(e.container).append(l.replace(/_INPUT_/,o))).find("label").attr("for","dt-search-"+Rt),s.find("input").attr("id","dt-search-"+Rt),Rt++,u=function(e){var t=this.value;r.return&&"Enter"!==e.key||t!=r.search&&Ve(n,a.processing,function(){r.search=t,Re(n,r),n._iDisplayStart=0,S(n)})},c=null!==n.searchDelay?n.searchDelay:0,d=H("input",s).val(r.search).attr("placeholder",a.placeholder).on("keyup.DT search.DT input.DT paste.DT cut.DT",c?V.util.debounce(u,c):u).on("mouseup.DT",function(e){setTimeout(function(){u.call(d[0],e)},10)}).on("keypress.DT",function(e){if(13==e.keyCode)return!1}).attr("aria-controls",t),H(n.nTable).on("search.dt.DT",function(e,t){n===t&&d[0]!==_.activeElement&&d.val("function"!=typeof r.search?r.search:"")}),s):null},"f"),V.feature.register("paging",function(e,t){if(!e.oFeatures.bPaginate)return null;t=H.extend({buttons:V.ext.pager.numbers_length,type:e.sPaginationType,boundaryNumbers:!0,firstLast:!0,previousNext:!0,numbers:!0},t);function n(){!function e(t,n,a){if(!t._bInitComplete)return;var r=a.type?V.ext.pager[a.type]:Ot,o=t.oLanguage.oAria.paginate||{},i=t._iDisplayStart,l=t._iDisplayLength,s=t.fnRecordsDisplay(),u=-1===l,c=u?0:Math.ceil(i/l),d=u?1:Math.ceil(s/l),f=r(a).map(function(e){return"numbers"===e?Et(c,d,a.buttons,a.boundaryNumbers):e}).flat();var h=[];for(var p=0;p<f.length;p++){var g=f[p],v=Pt(t,g,c,d),m=at(t,"pagingButton")(t,g,v.display,v.active,v.disabled),b="string"==typeof g?o[g]:o.number?o.number+(g+1):null;H(m.clicker).attr({"aria-controls":t.sTableId,"aria-disabled":v.disabled?"true":null,"aria-current":v.active?"page":null,"aria-label":b,"data-dt-idx":g,tabIndex:v.disabled?-1:t.iTabIndex||null}),"number"!=typeof g&&H(m.clicker).addClass(g),tt(m.clicker,{action:g},function(e){e.preventDefault(),Xe(t,e.data.action,!0)}),h.push(m.display)}i=at(t,"pagingContainer")(t,h);u=n.find(_.activeElement).data("dt-idx");n.empty().append(i);void 0!==u&&n.find("[data-dt-idx="+u+"]").trigger("focus");h.length&&1<a.buttons&&H(n).height()>=2*H(h[0]).outerHeight()-10&&e(t,n,H.extend({},a,{buttons:a.buttons-2}))}(e,a.children(),t)}var a=H("<div/>").addClass(e.oClasses.paging.container+(t.type?" paging_"+t.type:"")).append("<nav>");return e.aoDrawCallback.push(n),H(e.nTable).on("column-sizing.dt.DT",n),a},"p");var kt=0;return V.feature.register("pageLength",function(a,e){var t=a.oFeatures;if(!t.bPaginate||!t.bLengthChange)return null;e=H.extend({menu:a.aLengthMenu,text:a.oLanguage.sLengthMenu},e);var t=a.oClasses.length,n=a.sTableId,r=e.menu,o=[],i=[];if(Array.isArray(r[0]))o=r[0],i=r[1];else for(p=0;p<r.length;p++)H.isPlainObject(r[p])?(o.push(r[p].value),i.push(r[p].label)):(o.push(r[p]),i.push(r[p]));for(var l=e.text.match(/_MENU_$/),s=e.text.match(/^_MENU_/),u=e.text.replace(/_MENU_/,""),e="<label>"+e.text+"</label>",s=(s?e="_MENU_<label>"+u+"</label>":l&&(e="<label>"+u+"</label>_MENU_"),"tmp-"+ +new Date),c=H("<div/>").addClass(t.container).append(e.replace("_MENU_",'<span id="'+s+'"></span>')),d=[],f=(c.find("label")[0].childNodes.forEach(function(e){e.nodeType===Node.TEXT_NODE&&d.push({el:e,text:e.textContent})}),function(t){d.forEach(function(e){e.el.textContent=rt(a,e.text,t)})}),h=H("<select/>",{name:n+"_length","aria-controls":n,class:t.select}),p=0;p<o.length;p++)h[0][p]=new Option("number"==typeof i[p]?a.fnFormatNumber(i[p]):i[p],o[p]);return c.find("label").attr("for","dt-length-"+kt),h.attr("id","dt-length-"+kt),kt++,c.find("#"+s).replaceWith(h),H("select",c).val(a._iDisplayLength).on("change.DT",function(){We(a,H(this).val()),S(a)}),H(a.nTable).on("length.dt.DT",function(e,t,n){a===t&&(H("select",c).val(n),f(n))}),f(a._iDisplayLength),c},"l"),((H.fn.dataTable=V).$=H).fn.dataTableSettings=V.settings,H.fn.dataTableExt=V.ext,H.fn.DataTable=function(e){return H(this).dataTable(e).api()},H.each(V,function(e,t){H.fn.DataTable[e]=t}),V});
-
-/*! DataTables styling integration
- * © SpryMedia Ltd - datatables.net/license
- */
-!function(t){var o,d;"function"==typeof define&&define.amd?define(["jquery","datatables.net"],function(e){return t(e,window,document)}):"object"==typeof exports?(o=require("jquery"),d=function(e,n){n.fn.dataTable||require("datatables.net")(e,n)},"undefined"==typeof window?module.exports=function(e,n){return e=e||window,n=n||o(e),d(e,n),t(n,0,e.document)}:(d(window,o),module.exports=t(o,window,window.document))):t(jQuery,window,document)}(function(e,n,t){"use strict";return e.fn.dataTable});
-
-/*! Buttons for DataTables 3.1.1
- * © SpryMedia Ltd - datatables.net/license
- */
-!function(e){var o,i;"function"==typeof define&&define.amd?define(["jquery","datatables.net"],function(t){return e(t,window,document)}):"object"==typeof exports?(o=require("jquery"),i=function(t,n){n.fn.dataTable||require("datatables.net")(t,n)},"undefined"==typeof window?module.exports=function(t,n){return t=t||window,n=n||o(t),i(t,n),e(n,t,t.document)}:(i(window,o),module.exports=e(o,window,window.document))):e(jQuery,window,document)}(function(x,g,m){"use strict";var e=x.fn.dataTable,o=0,C=0,w=e.ext.buttons,i=null;function v(t,n,e){x.fn.animate?t.stop().fadeIn(n,e):(t.css("display","block"),e&&e.call(t))}function y(t,n,e){x.fn.animate?t.stop().fadeOut(n,e):(t.css("display","none"),e&&e.call(t))}function _(n,t){if(!e.versionCheck("2"))throw"Warning: Buttons requires DataTables 2 or newer";if(!(this instanceof _))return function(t){return new _(t,n).container()};!0===(t=void 0===t?{}:t)&&(t={}),Array.isArray(t)&&(t={buttons:t}),this.c=x.extend(!0,{},_.defaults,t),t.buttons&&(this.c.buttons=t.buttons),this.s={dt:new e.Api(n),buttons:[],listenKeys:"",namespace:"dtb"+o++},this.dom={container:x("<"+this.c.dom.container.tag+"/>").addClass(this.c.dom.container.className)},this._constructor()}x.extend(_.prototype,{action:function(t,n){t=this._nodeToButton(t);return void 0===n?t.conf.action:(t.conf.action=n,this)},active:function(t,n){var t=this._nodeToButton(t),e=this.c.dom.button.active,o=x(t.node);return t.inCollection&&this.c.dom.collection.button&&void 0!==this.c.dom.collection.button.active&&(e=this.c.dom.collection.button.active),void 0===n?o.hasClass(e):(o.toggleClass(e,void 0===n||n),this)},add:function(t,n,e){var o=this.s.buttons;if("string"==typeof n){for(var i=n.split("-"),s=this.s,r=0,a=i.length-1;r<a;r++)s=s.buttons[+i[r]];o=s.buttons,n=+i[i.length-1]}return this._expandButton(o,t,void 0!==t?t.split:void 0,(void 0===t||void 0===t.split||0===t.split.length)&&void 0!==s,!1,n),void 0!==e&&!0!==e||this._draw(),this},collectionRebuild:function(t,n){var e=this._nodeToButton(t);if(void 0!==n){for(var o=e.buttons.length-1;0<=o;o--)this.remove(e.buttons[o].node);for(e.conf.prefixButtons&&n.unshift.apply(n,e.conf.prefixButtons),e.conf.postfixButtons&&n.push.apply(n,e.conf.postfixButtons),o=0;o<n.length;o++){var i=n[o];this._expandButton(e.buttons,i,void 0!==i&&void 0!==i.config&&void 0!==i.config.split,!0,void 0!==i.parentConf&&void 0!==i.parentConf.split,null,i.parentConf)}}this._draw(e.collection,e.buttons)},container:function(){return this.dom.container},disable:function(t){t=this._nodeToButton(t);return x(t.node).addClass(this.c.dom.button.disabled).prop("disabled",!0),this},destroy:function(){x("body").off("keyup."+this.s.namespace);for(var t=this.s.buttons.slice(),n=0,e=t.length;n<e;n++)this.remove(t[n].node);this.dom.container.remove();var o=this.s.dt.settings()[0];for(n=0,e=o.length;n<e;n++)if(o.inst===this){o.splice(n,1);break}return this},enable:function(t,n){return!1===n?this.disable(t):(n=this._nodeToButton(t),x(n.node).removeClass(this.c.dom.button.disabled).prop("disabled",!1),this)},index:function(t,n,e){n||(n="",e=this.s.buttons);for(var o=0,i=e.length;o<i;o++){var s=e[o].buttons;if(e[o].node===t)return n+o;if(s&&s.length){s=this.index(t,o+"-",s);if(null!==s)return s}}return null},name:function(){return this.c.name},node:function(t){return t?(t=this._nodeToButton(t),x(t.node)):this.dom.container},processing:function(t,n){var e=this.s.dt,o=this._nodeToButton(t);return void 0===n?x(o.node).hasClass("processing"):(x(o.node).toggleClass("processing",n),x(e.table().node()).triggerHandler("buttons-processing.dt",[n,e.button(t),e,x(t),o.conf]),this)},remove:function(t){var n=this._nodeToButton(t),e=this._nodeToHost(t),o=this.s.dt;if(n.buttons.length)for(var i=n.buttons.length-1;0<=i;i--)this.remove(n.buttons[i].node);n.conf.destroying=!0,n.conf.destroy&&n.conf.destroy.call(o.button(t),o,x(t),n.conf),this._removeKey(n.conf),x(n.node).remove();o=x.inArray(n,e);return e.splice(o,1),this},text:function(t,n){function e(t){return"function"==typeof t?t(i,s,o.conf):t}var o=this._nodeToButton(t),t=o.textNode,i=this.s.dt,s=x(o.node);return void 0===n?e(o.conf.text):(o.conf.text=n,t.html(e(n)),this)},_constructor:function(){var e=this,t=this.s.dt,o=t.settings()[0],n=this.c.buttons;o._buttons||(o._buttons=[]),o._buttons.push({inst:this,name:this.c.name});for(var i=0,s=n.length;i<s;i++)this.add(n[i]);t.on("destroy",function(t,n){n===o&&e.destroy()}),x("body").on("keyup."+this.s.namespace,function(t){var n;m.activeElement&&m.activeElement!==m.body||(n=String.fromCharCode(t.keyCode).toLowerCase(),-1!==e.s.listenKeys.toLowerCase().indexOf(n)&&e._keypress(n,t))})},_addKey:function(t){t.key&&(this.s.listenKeys+=(x.isPlainObject(t.key)?t.key:t).key)},_draw:function(t,n){t||(t=this.dom.container,n=this.s.buttons),t.children().detach();for(var e=0,o=n.length;e<o;e++)t.append(n[e].inserter),t.append(" "),n[e].buttons&&n[e].buttons.length&&this._draw(n[e].collection,n[e].buttons)},_expandButton:function(t,n,e,o,i,s,r){for(var a,l=this.s.dt,c=this.c.dom.collection,u=Array.isArray(n)?n:[n],d=0,f=(u=void 0===n?Array.isArray(e)?e:[e]:u).length;d<f;d++){var p=this._resolveExtends(u[d]);if(p)if(a=!(!p.config||!p.config.split),Array.isArray(p))this._expandButton(t,p,void 0!==h&&void 0!==h.conf?h.conf.split:void 0,o,void 0!==r&&void 0!==r.split,s,r);else{var h=this._buildButton(p,o,void 0!==p.split||void 0!==p.config&&void 0!==p.config.split,i);if(h){if(null!=s?(t.splice(s,0,h),s++):t.push(h),h.conf.buttons&&(h.collection=x("<"+c.container.content.tag+"/>"),h.conf._collection=h.collection,x(h.node).append(c.action.dropHtml),this._expandButton(h.buttons,h.conf.buttons,h.conf.split,!a,a,s,h.conf)),h.conf.split){h.collection=x("<"+c.container.tag+"/>"),h.conf._collection=h.collection;for(var b=0;b<h.conf.split.length;b++){var g=h.conf.split[b];"object"==typeof g&&(g.parent=r,void 0===g.collectionLayout&&(g.collectionLayout=h.conf.collectionLayout),void 0===g.dropup&&(g.dropup=h.conf.dropup),void 0===g.fade)&&(g.fade=h.conf.fade)}this._expandButton(h.buttons,h.conf.buttons,h.conf.split,!a,a,s,h.conf)}h.conf.parent=r,p.init&&p.init.call(l.button(h.node),l,x(h.node),p)}}}},_buildButton:function(n,t,e,o){function i(t){return"function"==typeof t?t(f,c,n):t}var s,r,a,l,c,u=this,d=this.c.dom,f=this.s.dt,p=x.extend(!0,{},d.button);if(t&&e&&d.collection.split?x.extend(!0,p,d.collection.split.action):o||t?x.extend(!0,p,d.collection.button):e&&x.extend(!0,p,d.split.button),n.spacer)return d=x("<"+p.spacer.tag+"/>").addClass("dt-button-spacer "+n.style+" "+p.spacer.className).html(i(n.text)),{conf:n,node:d,inserter:d,buttons:[],inCollection:t,isSplit:e,collection:null,textNode:d};if(n.available&&!n.available(f,n)&&!n.html)return!1;n.html?c=x(n.html):(r=function(t,n,e,o,i){o.action.call(n.button(e),t,n,e,o,i),x(n.table().node()).triggerHandler("buttons-action.dt",[n.button(e),n,e,o])},a=function(t,n,e,o){o.async?(u.processing(e[0],!0),setTimeout(function(){r(t,n,e,o,function(){u.processing(e[0],!1)})},o.async)):r(t,n,e,o,function(){})},d=n.tag||p.tag,l=void 0===n.clickBlurs||n.clickBlurs,c=x("<"+d+"/>").addClass(p.className).attr("tabindex",this.s.dt.settings()[0].iTabIndex).attr("aria-controls",this.s.dt.table().node().id).on("click.dtb",function(t){t.preventDefault(),!c.hasClass(p.disabled)&&n.action&&a(t,f,c,n),l&&c.trigger("blur")}).on("keypress.dtb",function(t){13===t.keyCode&&(t.preventDefault(),!c.hasClass(p.disabled))&&n.action&&a(t,f,c,n)}),"a"===d.toLowerCase()&&c.attr("href","#"),"button"===d.toLowerCase()&&c.attr("type","button"),s=p.liner.tag?(d=x("<"+p.liner.tag+"/>").html(i(n.text)).addClass(p.liner.className),"a"===p.liner.tag.toLowerCase()&&d.attr("href","#"),c.append(d),d):(c.html(i(n.text)),c),!1===n.enabled&&c.addClass(p.disabled),n.className&&c.addClass(n.className),n.titleAttr&&c.attr("title",i(n.titleAttr)),n.attr&&c.attr(n.attr),n.namespace||(n.namespace=".dt-button-"+C++),void 0!==n.config&&n.config.split&&(n.split=n.config.split));var h,b,g,m,v,y,d=this.c.dom.buttonContainer,d=d&&d.tag?x("<"+d.tag+"/>").addClass(d.className).append(c):c;return this._addKey(n),this.c.buttonCreated&&(d=this.c.buttonCreated(n,d)),e&&(b=(h=t?x.extend(!0,this.c.dom.split,this.c.dom.collection.split):this.c.dom.split).wrapper,g=x("<"+b.tag+"/>").addClass(b.className).append(c),m=x.extend(n,{align:h.dropdown.align,attr:{"aria-haspopup":"dialog","aria-expanded":!1},className:h.dropdown.className,closeButton:!1,splitAlignClass:h.dropdown.splitAlignClass,text:h.dropdown.text}),this._addKey(m),v=function(t,n,e,o){w.split.action.call(n.button(g),t,n,e,o),x(n.table().node()).triggerHandler("buttons-action.dt",[n.button(e),n,e,o]),e.attr("aria-expanded",!0)},y=x('<button class="'+h.dropdown.className+' dt-button"></button>').html(h.dropdown.dropHtml).on("click.dtb",function(t){t.preventDefault(),t.stopPropagation(),y.hasClass(p.disabled)||v(t,f,y,m),l&&y.trigger("blur")}).on("keypress.dtb",function(t){13===t.keyCode&&(t.preventDefault(),y.hasClass(p.disabled)||v(t,f,y,m))}),0===n.split.length&&y.addClass("dtb-hide-drop"),g.append(y).attr(m.attr)),{conf:n,node:(e?g:c).get(0),inserter:e?g:d,buttons:[],inCollection:t,isSplit:e,inSplit:o,collection:null,textNode:s}},_nodeToButton:function(t,n){for(var e=0,o=(n=n||this.s.buttons).length;e<o;e++){if(n[e].node===t)return n[e];if(n[e].buttons.length){var i=this._nodeToButton(t,n[e].buttons);if(i)return i}}},_nodeToHost:function(t,n){for(var e=0,o=(n=n||this.s.buttons).length;e<o;e++){if(n[e].node===t)return n;if(n[e].buttons.length){var i=this._nodeToHost(t,n[e].buttons);if(i)return i}}},_keypress:function(s,r){var a;r._buttonsHandled||(a=function(t){for(var n,e,o=0,i=t.length;o<i;o++)n=t[o].conf,e=t[o].node,!n.key||n.key!==s&&(!x.isPlainObject(n.key)||n.key.key!==s||n.key.shiftKey&&!r.shiftKey||n.key.altKey&&!r.altKey||n.key.ctrlKey&&!r.ctrlKey||n.key.metaKey&&!r.metaKey)||(r._buttonsHandled=!0,x(e).click()),t[o].buttons.length&&a(t[o].buttons)})(this.s.buttons)},_removeKey:function(t){var n;t.key&&(t=(x.isPlainObject(t.key)?t.key:t).key,n=this.s.listenKeys.split(""),t=x.inArray(t,n),n.splice(t,1),this.s.listenKeys=n.join(""))},_resolveExtends:function(e){function t(t){for(var n=0;!x.isPlainObject(t)&&!Array.isArray(t);){if(void 0===t)return;if("function"==typeof t){if(!(t=t.call(i,s,e)))return!1}else if("string"==typeof t){if(!w[t])return{html:t};t=w[t]}if(30<++n)throw"Buttons: Too many iterations"}return Array.isArray(t)?t:x.extend({},t)}var n,o,i=this,s=this.s.dt;for(e=t(e);e&&e.extend;){if(!w[e.extend])throw"Cannot extend unknown button type: "+e.extend;var r=t(w[e.extend]);if(Array.isArray(r))return r;if(!r)return!1;var a=r.className;void 0!==e.config&&void 0!==r.config&&(e.config=x.extend({},r.config,e.config)),e=x.extend({},r,e),a&&e.className!==a&&(e.className=a+" "+e.className),e.extend=r.extend}var l=e.postfixButtons;if(l)for(e.buttons||(e.buttons=[]),n=0,o=l.length;n<o;n++)e.buttons.push(l[n]);var c=e.prefixButtons;if(c)for(e.buttons||(e.buttons=[]),n=0,o=c.length;n<o;n++)e.buttons.splice(n,0,c[n]);return e},_popover:function(o,t,n){function i(){f=!0,y(x(h),p.fade,function(){x(this).detach()}),x(u.buttons('[aria-haspopup="dialog"][aria-expanded="true"]').nodes()).attr("aria-expanded","false"),x("div.dt-button-background").off("click.dtb-collection"),_.background(!1,p.backgroundClassName,p.fade,b),x(g).off("resize.resize.dtb-collection"),x("body").off(".dtb-collection"),u.off("buttons-action.b-internal"),u.off("destroy")}var e,s,r,a,l,c,u=t,d=this.c,f=!1,p=x.extend({align:"button-left",autoClose:!1,background:!0,backgroundClassName:"dt-button-background",closeButton:!0,containerClassName:d.dom.collection.container.className,contentClassName:d.dom.collection.container.content.className,collectionLayout:"",collectionTitle:"",dropup:!1,fade:400,popoverTitle:"",rightAlignClassName:"dt-button-right",tag:d.dom.collection.container.tag},n),h=p.tag+"."+p.containerClassName.replace(/ /g,"."),b=t.node();!1===o?i():((d=x(u.buttons('[aria-haspopup="dialog"][aria-expanded="true"]').nodes())).length&&(b.closest(h).length&&(b=d.eq(0)),i()),n=x(".dt-button",o).length,d="",3===n?d="dtb-b3":2===n?d="dtb-b2":1===n&&(d="dtb-b1"),e=x("<"+p.tag+"/>").addClass(p.containerClassName).addClass(p.collectionLayout).addClass(p.splitAlignClass).addClass(d).css("display","none").attr({"aria-modal":!0,role:"dialog"}),o=x(o).addClass(p.contentClassName).attr("role","menu").appendTo(e),b.attr("aria-expanded","true"),b.parents("body")[0]!==m.body&&(b=m.body.lastChild),p.popoverTitle?e.prepend('<div class="dt-button-collection-title">'+p.popoverTitle+"</div>"):p.collectionTitle&&e.prepend('<div class="dt-button-collection-title">'+p.collectionTitle+"</div>"),p.closeButton&&e.prepend('<div class="dtb-popover-close">×</div>').addClass("dtb-collection-closeable"),v(e.insertAfter(b),p.fade),n=x(t.table().container()),d=e.css("position"),"container"!==p.span&&"dt-container"!==p.align||(b=b.parent(),e.css("width",n.width())),"absolute"===d?(t=x(b[0].offsetParent),n=b.position(),d=b.offset(),a=t.offset(),s=t.position(),r=g.getComputedStyle(t[0]),a.height=t.outerHeight(),a.width=t.width()+parseFloat(r.paddingLeft),a.right=a.left+a.width,a.bottom=a.top+a.height,t=n.top+b.outerHeight(),a=n.left,e.css({top:t,left:a}),r=g.getComputedStyle(e[0]),(l=e.offset()).height=e.outerHeight(),l.width=e.outerWidth(),l.right=l.left+l.width,l.bottom=l.top+l.height,l.marginTop=parseFloat(r.marginTop),l.marginBottom=parseFloat(r.marginBottom),p.dropup&&(t=n.top-l.height-l.marginTop-l.marginBottom),"button-right"!==p.align&&!e.hasClass(p.rightAlignClassName)||(a=n.left-l.width+b.outerWidth()),"dt-container"!==p.align&&"container"!==p.align||a<n.left&&(a=-n.left),s.left+a+l.width>x(g).width()&&(a=x(g).width()-l.width-s.left),d.left+a<0&&(a=-d.left),s.top+t+l.height>x(g).height()+x(g).scrollTop()&&(t=n.top-l.height-l.marginTop-l.marginBottom),s.top+t<x(g).scrollTop()&&(t=n.top+b.outerHeight()),e.css({top:t,left:a})):((c=function(){var t=x(g).height()/2,n=e.height()/2;e.css("marginTop",-1*(n=t<n?t:n))})(),x(g).on("resize.dtb-collection",function(){c()})),p.background&&_.background(!0,p.backgroundClassName,p.fade,p.backgroundHost||b),x("div.dt-button-background").on("click.dtb-collection",function(){}),p.autoClose&&setTimeout(function(){u.on("buttons-action.b-internal",function(t,n,e,o){o[0]!==b[0]&&i()})},0),x(e).trigger("buttons-popover.dt"),u.on("destroy",i),setTimeout(function(){f=!1,x("body").on("click.dtb-collection",function(t){var n,e;!f&&(n=x.fn.addBack?"addBack":"andSelf",e=x(t.target).parent()[0],!x(t.target).parents()[n]().filter(o).length&&!x(e).hasClass("dt-buttons")||x(t.target).hasClass("dt-button-background"))&&i()}).on("keyup.dtb-collection",function(t){27===t.keyCode&&i()}).on("keydown.dtb-collection",function(t){var n=x("a, button",o),e=m.activeElement;9===t.keyCode&&(-1===n.index(e)?(n.first().focus(),t.preventDefault()):t.shiftKey?e===n[0]&&(n.last().focus(),t.preventDefault()):e===n.last()[0]&&(n.first().focus(),t.preventDefault()))})},0))}}),_.background=function(t,n,e,o){void 0===e&&(e=400),o=o||m.body,t?v(x("<div/>").addClass(n).css("display","none").insertAfter(o),e):y(x("div."+n),e,function(){x(this).removeClass(n).remove()})},_.instanceSelector=function(t,s){var r,a,l;return null==t?x.map(s,function(t){return t.inst}):(r=[],a=x.map(s,function(t){return t.name}),(l=function(t){var n;if(Array.isArray(t))for(var e=0,o=t.length;e<o;e++)l(t[e]);else if("string"==typeof t)-1!==t.indexOf(",")?l(t.split(",")):-1!==(n=x.inArray(t.trim(),a))&&r.push(s[n].inst);else if("number"==typeof t)r.push(s[t].inst);else if("object"==typeof t&&t.nodeName)for(var i=0;i<s.length;i++)s[i].inst.dom.container[0]===t&&r.push(s[i].inst);else"object"==typeof t&&r.push(t)})(t),r)},_.buttonSelector=function(t,n){for(var c=[],u=function(t,n,e){for(var o,i,s=0,r=n.length;s<r;s++)(o=n[s])&&(t.push({node:o.node,name:o.conf.name,idx:i=void 0!==e?e+s:s+""}),o.buttons)&&u(t,o.buttons,i+"-")},d=function(t,n){var e=[],o=(u(e,n.s.buttons),x.map(e,function(t){return t.node}));if(Array.isArray(t)||t instanceof x)for(s=0,r=t.length;s<r;s++)d(t[s],n);else if(null==t||"*"===t)for(s=0,r=e.length;s<r;s++)c.push({inst:n,node:e[s].node});else if("number"==typeof t)n.s.buttons[t]&&c.push({inst:n,node:n.s.buttons[t].node});else if("string"==typeof t)if(-1!==t.indexOf(","))for(var i=t.split(","),s=0,r=i.length;s<r;s++)d(i[s].trim(),n);else if(t.match(/^\d+(\-\d+)*$/)){var a=x.map(e,function(t){return t.idx});c.push({inst:n,node:e[x.inArray(t,a)].node})}else if(-1!==t.indexOf(":name")){var l=t.replace(":name","");for(s=0,r=e.length;s<r;s++)e[s].name===l&&c.push({inst:n,node:e[s].node})}else x(o).filter(t).each(function(){c.push({inst:n,node:this})});else"object"==typeof t&&t.nodeName&&-1!==(a=x.inArray(t,o))&&c.push({inst:n,node:o[a]})},e=0,o=t.length;e<o;e++){var i=t[e];d(n,i)}return c},_.stripData=function(t,n){return t="string"==typeof t&&(t=_.stripHtmlScript(t),t=_.stripHtmlComments(t),n&&!n.stripHtml||(t=e.util.stripHtml(t)),n&&!n.trim||(t=t.trim()),n&&!n.stripNewlines||(t=t.replace(/\n/g," ")),!n||n.decodeEntities)?i?i(t):(c.innerHTML=t,c.value):t},_.entityDecoder=function(t){i=t},_.stripHtmlComments=function(t){for(var n;(t=(n=t).replace(/(<!--.*?--!?>)|(<!--[\S\s]+?--!?>)|(<!--[\S\s]*?$)/g,""))!==n;);return t},_.stripHtmlScript=function(t){for(var n;(t=(n=t).replace(/<script\b[^<]*(?:(?!<\/script[^>]*>)<[^<]*)*<\/script[^>]*>/gi,""))!==n;);return t},_.defaults={buttons:["copy","excel","csv","pdf","print"],name:"main",tabIndex:0,dom:{container:{tag:"div",className:"dt-buttons"},collection:{action:{dropHtml:'<span class="dt-button-down-arrow">▼</span>'},container:{className:"dt-button-collection",content:{className:"",tag:"div"},tag:"div"}},button:{tag:"button",className:"dt-button",active:"dt-button-active",disabled:"disabled",spacer:{className:"dt-button-spacer",tag:"span"},liner:{tag:"span",className:""}},split:{action:{className:"dt-button-split-drop-button dt-button",tag:"button"},dropdown:{align:"split-right",className:"dt-button-split-drop",dropHtml:'<span class="dt-button-down-arrow">▼</span>',splitAlignClass:"dt-button-split-left",tag:"button"},wrapper:{className:"dt-button-split",tag:"div"}}}},x.extend(w,{collection:{text:function(t){return t.i18n("buttons.collection","Collection")},className:"buttons-collection",closeButton:!(_.version="3.1.1"),init:function(t,n){n.attr("aria-expanded",!1)},action:function(t,n,e,o){o._collection.parents("body").length?this.popover(!1,o):this.popover(o._collection,o),"keypress"===t.type&&x("a, button",o._collection).eq(0).focus()},attr:{"aria-haspopup":"dialog"}},split:{text:function(t){return t.i18n("buttons.split","Split")},className:"buttons-split",closeButton:!1,init:function(t,n){return n.attr("aria-expanded",!1)},action:function(t,n,e,o){this.popover(o._collection,o)},attr:{"aria-haspopup":"dialog"}},copy:function(){if(w.copyHtml5)return"copyHtml5"},csv:function(t,n){if(w.csvHtml5&&w.csvHtml5.available(t,n))return"csvHtml5"},excel:function(t,n){if(w.excelHtml5&&w.excelHtml5.available(t,n))return"excelHtml5"},pdf:function(t,n){if(w.pdfHtml5&&w.pdfHtml5.available(t,n))return"pdfHtml5"},pageLength:function(t){var n=t.settings()[0].aLengthMenu,e=[],o=[];if(Array.isArray(n[0]))e=n[0],o=n[1];else for(var i=0;i<n.length;i++){var s=n[i];x.isPlainObject(s)?(e.push(s.value),o.push(s.label)):(e.push(s),o.push(s))}return{extend:"collection",text:function(t){return t.i18n("buttons.pageLength",{"-1":"Show all rows",_:"Show %d rows"},t.page.len())},className:"buttons-page-length",autoClose:!0,buttons:x.map(e,function(s,t){return{text:o[t],className:"button-page-length",action:function(t,n){n.page.len(s).draw()},init:function(t,n,e){function o(){i.active(t.page.len()===s)}var i=this;t.on("length.dt"+e.namespace,o),o()},destroy:function(t,n,e){t.off("length.dt"+e.namespace)}}}),init:function(t,n,e){var o=this;t.on("length.dt"+e.namespace,function(){o.text(e.text)})},destroy:function(t,n,e){t.off("length.dt"+e.namespace)}}},spacer:{style:"empty",spacer:!0,text:function(t){return t.i18n("buttons.spacer","")}}}),e.Api.register("buttons()",function(n,e){void 0===e&&(e=n,n=void 0),this.selector.buttonGroup=n;var t=this.iterator(!0,"table",function(t){if(t._buttons)return _.buttonSelector(_.instanceSelector(n,t._buttons),e)},!0);return t._groupSelector=n,t}),e.Api.register("button()",function(t,n){t=this.buttons(t,n);return 1<t.length&&t.splice(1,t.length),t}),e.Api.registerPlural("buttons().active()","button().active()",function(n){return void 0===n?this.map(function(t){return t.inst.active(t.node)}):this.each(function(t){t.inst.active(t.node,n)})}),e.Api.registerPlural("buttons().action()","button().action()",function(n){return void 0===n?this.map(function(t){return t.inst.action(t.node)}):this.each(function(t){t.inst.action(t.node,n)})}),e.Api.registerPlural("buttons().collectionRebuild()","button().collectionRebuild()",function(e){return this.each(function(t){for(var n=0;n<e.length;n++)"object"==typeof e[n]&&(e[n].parentConf=t);t.inst.collectionRebuild(t.node,e)})}),e.Api.register(["buttons().enable()","button().enable()"],function(n){return this.each(function(t){t.inst.enable(t.node,n)})}),e.Api.register(["buttons().disable()","button().disable()"],function(){return this.each(function(t){t.inst.disable(t.node)})}),e.Api.register("button().index()",function(){var n=null;return this.each(function(t){t=t.inst.index(t.node);null!==t&&(n=t)}),n}),e.Api.registerPlural("buttons().nodes()","button().node()",function(){var n=x();return x(this.each(function(t){n=n.add(t.inst.node(t.node))})),n}),e.Api.registerPlural("buttons().processing()","button().processing()",function(n){return void 0===n?this.map(function(t){return t.inst.processing(t.node)}):this.each(function(t){t.inst.processing(t.node,n)})}),e.Api.registerPlural("buttons().text()","button().text()",function(n){return void 0===n?this.map(function(t){return t.inst.text(t.node)}):this.each(function(t){t.inst.text(t.node,n)})}),e.Api.registerPlural("buttons().trigger()","button().trigger()",function(){return this.each(function(t){t.inst.node(t.node).trigger("click")})}),e.Api.register("button().popover()",function(n,e){return this.map(function(t){return t.inst._popover(n,this.button(this[0].node),e)})}),e.Api.register("buttons().containers()",function(){var i=x(),s=this._groupSelector;return this.iterator(!0,"table",function(t){if(t._buttons)for(var n=_.instanceSelector(s,t._buttons),e=0,o=n.length;e<o;e++)i=i.add(n[e].container())}),i}),e.Api.register("buttons().container()",function(){return this.containers().eq(0)}),e.Api.register("button().add()",function(t,n,e){var o=this.context;return o.length&&(o=_.instanceSelector(this._groupSelector,o[0]._buttons)).length&&o[0].add(n,t,e),this.button(this._groupSelector,t)}),e.Api.register("buttons().destroy()",function(){return this.pluck("inst").unique().each(function(t){t.destroy()}),this}),e.Api.registerPlural("buttons().remove()","buttons().remove()",function(){return this.each(function(t){t.inst.remove(t.node)}),this}),e.Api.register("buttons.info()",function(t,n,e){var o=this;return!1===t?(this.off("destroy.btn-info"),y(x("#datatables_buttons_info"),400,function(){x(this).remove()}),clearTimeout(s),s=null):(s&&clearTimeout(s),x("#datatables_buttons_info").length&&x("#datatables_buttons_info").remove(),t=t?"<h2>"+t+"</h2>":"",v(x('<div id="datatables_buttons_info" class="dt-button-info"/>').html(t).append(x("<div/>")["string"==typeof n?"html":"append"](n)).css("display","none").appendTo("body")),void 0!==e&&0!==e&&(s=setTimeout(function(){o.buttons.info(!1)},e)),this.on("destroy.btn-info",function(){o.buttons.info(!1)})),this}),e.Api.register("buttons.exportData()",function(t){if(this.context.length)return u(new e.Api(this.context[0]),t)}),e.Api.register("buttons.exportInfo()",function(t){return{filename:n(t=t||{},this),title:a(t,this),messageTop:l(this,t,t.message||t.messageTop,"top"),messageBottom:l(this,t,t.messageBottom,"bottom")}});var s,n=function(t,n){var e;return null==(e="function"==typeof(e="*"===t.filename&&"*"!==t.title&&void 0!==t.title&&null!==t.title&&""!==t.title?t.title:t.filename)?e(t,n):e)?null:(e=(e=-1!==e.indexOf("*")?e.replace(/\*/g,x("head > title").text()).trim():e).replace(/[^a-zA-Z0-9_\u00A1-\uFFFF\.,\-_ !\(\)]/g,""))+(r(t.extension,t,n)||"")},r=function(t,n,e){return null==t?null:"function"==typeof t?t(n,e):t},a=function(t,n){t=r(t.title,t,n);return null===t?null:-1!==t.indexOf("*")?t.replace(/\*/g,x("head > title").text()||""):t},l=function(t,n,e,o){e=r(e,n,t);return null===e?null:(n=x("caption",t.table().container()).eq(0),"*"===e?n.css("caption-side")!==o?null:n.length?n.text():"":e)},c=x("<textarea/>")[0],u=function(i,t){for(var s=x.extend(!0,{},{rows:null,columns:"",modifier:{search:"applied",order:"applied"},orthogonal:"display",stripHtml:!0,stripNewlines:!0,decodeEntities:!0,trim:!0,format:{header:function(t){return _.stripData(t,s)},footer:function(t){return _.stripData(t,s)},body:function(t){return _.stripData(t,s)}},customizeData:null,customizeZip:null},t),t=i.columns(s.columns).indexes().map(function(t){var n=i.column(t);return s.format.header(n.title(),t,n.header())}).toArray(),n=i.table().footer()?i.columns(s.columns).indexes().map(function(t){var n,e=i.column(t).footer(),o="";return e&&(o=((n=x(".dt-column-title",e)).length?n:x(e)).html()),s.format.footer(o,t,e)}).toArray():null,e=x.extend({},s.modifier),o=(i.select&&"function"==typeof i.select.info&&void 0===e.selected&&i.rows(s.rows,x.extend({selected:!0},e)).any()&&x.extend(e,{selected:!0}),i.rows(s.rows,e).indexes().toArray()),o=i.cells(o,s.columns,{order:e.order}),r=o.render(s.orthogonal).toArray(),a=o.nodes().toArray(),l=o.indexes().toArray(),c=i.columns(s.columns).count(),u=[],d=0,f=0,p=0<c?r.length/c:0;f<p;f++){for(var h=[c],b=0;b<c;b++)h[b]=s.format.body(r[d],l[d].row,l[d].column,a[d]),d++;u[f]=h}e={header:t,headerStructure:A(s.format.header,i.table().header.structure(s.columns)),footer:n,footerStructure:A(s.format.footer,i.table().footer.structure(s.columns)),body:u};return s.customizeData&&s.customizeData(e),e};function A(t,n){for(var e=0;e<n.length;e++)for(var o=0;o<n[e].length;o++){var i=n[e][o];i&&(i.title=t(i.title,o,i.cell))}return n}function t(t,n){t=new e.Api(t),n=n||t.init().buttons||e.defaults.buttons;return new _(t,n).container()}return x.fn.dataTable.Buttons=_,x.fn.DataTable.Buttons=_,x(m).on("init.dt plugin-init.dt",function(t,n){"dt"===t.namespace&&(t=n.oInit.buttons||e.defaults.buttons)&&!n._buttons&&new _(n,t).container()}),e.ext.feature.push({fnInit:t,cFeature:"B"}),e.feature&&e.feature.register("buttons",t),e});
-
-/*! DataTables styling wrapper for Buttons
- * © SpryMedia Ltd - datatables.net/license
- */
-!function(n){var o,d;"function"==typeof define&&define.amd?define(["jquery","datatables.net-dt","datatables.net-buttons"],function(e){return n(e,window,document)}):"object"==typeof exports?(o=require("jquery"),d=function(e,t){t.fn.dataTable||require("datatables.net-dt")(e,t),t.fn.dataTable.Buttons||require("datatables.net-buttons")(e,t)},"undefined"==typeof window?module.exports=function(e,t){return e=e||window,t=t||o(e),d(e,t),n(t,0,e.document)}:(d(window,o),module.exports=n(o,window,window.document))):n(jQuery,window,document)}(function(e,t,n){"use strict";return e.fn.dataTable});
-
-/*!
- * HTML5 export buttons for Buttons and DataTables.
- * © SpryMedia Ltd - datatables.net/license
- *
- * FileSaver.js (1.3.3) - MIT license
- * Copyright © 2016 Eli Grey - http://eligrey.com
- */
-!function(o){var l,n;"function"==typeof define&&define.amd?define(["jquery","datatables.net","datatables.net-buttons"],function(t){return o(t,window,document)}):"object"==typeof exports?(l=require("jquery"),n=function(t,e){e.fn.dataTable||require("datatables.net")(t,e),e.fn.dataTable.Buttons||require("datatables.net-buttons")(t,e)},"undefined"==typeof window?module.exports=function(t,e){return t=t||window,e=e||l(t),n(t,e),o(e,t,t.document)}:(n(window,l),module.exports=o(l,window,window.document))):o(jQuery,window,document)}(function(S,C,u){"use strict";var e,o,t=S.fn.dataTable;function T(){return e||C.JSZip}function m(){return o||C.pdfMake}t.Buttons.pdfMake=function(t){if(!t)return m();o=t},t.Buttons.jszip=function(t){if(!t)return T();e=t};function k(t){var e="Sheet1";return e=t.sheetName?t.sheetName.replace(/[\[\]\*\/\\\?\:]/g,""):e}function c(t,e){function o(t){for(var e="",o=0,l=t.length;o<l;o++)0<o&&(e+=a),e+=r?r+(""+t[o]).replace(d,p+r)+r:t[o];return e}var l=y(e),n=t.buttons.exportData(e.exportOptions),r=e.fieldBoundary,a=e.fieldSeparator,d=new RegExp(r,"g"),p=void 0!==e.escapeChar?e.escapeChar:"\\",t="",i="",f=[];e.header&&(t=n.headerStructure.map(function(t){return o(t.map(function(t){return t?t.title:""}))}).join(l)+l),e.footer&&n.footer&&(i=n.footerStructure.map(function(t){return o(t.map(function(t){return t?t.title:""}))}).join(l)+l);for(var m=0,s=n.body.length;m<s;m++)f.push(o(n.body[m]));return{str:t+f.join(l)+l+i,rows:f.length}}function s(){var t;return-1!==navigator.userAgent.indexOf("Safari")&&-1===navigator.userAgent.indexOf("Chrome")&&-1===navigator.userAgent.indexOf("Opera")&&!!((t=navigator.userAgent.match(/AppleWebKit\/(\d+\.\d+)/))&&1<t.length&&+t[1]<603.1)}var N=function(d){var p,i,f,m,s,u,e,c,y,l,t;if(!(void 0===d||"undefined"!=typeof navigator&&/MSIE [1-9]\./.test(navigator.userAgent)))return t=d.document,p=function(){return d.URL||d.webkitURL||d},i=t.createElementNS("http://www.w3.org/1999/xhtml","a"),f="download"in i,m=/constructor/i.test(d.HTMLElement)||d.safari,s=/CriOS\/[\d]+/.test(navigator.userAgent),u=function(t){(d.setImmediate||d.setTimeout)(function(){throw t},0)},e=4e4,c=function(t){setTimeout(function(){"string"==typeof t?p().revokeObjectURL(t):t.remove()},e)},y=function(t){return/^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(t.type)?new Blob([String.fromCharCode(65279),t],{type:t.type}):t},t=(l=function(t,o,e){e||(t=y(t));var l,n,r=this,e="application/octet-stream"===t.type,a=function(){for(var t=r,e="writestart progress write writeend".split(" "),o=void 0,l=(e=[].concat(e)).length;l--;){var n=t["on"+e[l]];if("function"==typeof n)try{n.call(t,o||t)}catch(t){u(t)}}};r.readyState=r.INIT,f?(l=p().createObjectURL(t),setTimeout(function(){var t,e;i.href=l,i.download=o,t=i,e=new MouseEvent("click"),t.dispatchEvent(e),a(),c(l),r.readyState=r.DONE})):(s||e&&m)&&d.FileReader?((n=new FileReader).onloadend=function(){var t=s?n.result:n.result.replace(/^data:[^;]*;/,"data:attachment/file;");d.open(t,"_blank")||(d.location.href=t),r.readyState=r.DONE,a()},n.readAsDataURL(t),r.readyState=r.INIT):(l=l||p().createObjectURL(t),!e&&d.open(l,"_blank")||(d.location.href=l),r.readyState=r.DONE,a(),c(l))}).prototype,"undefined"!=typeof navigator&&navigator.msSaveOrOpenBlob?function(t,e,o){return e=e||t.name||"download",o||(t=y(t)),navigator.msSaveOrOpenBlob(t,e)}:(t.abort=function(){},t.readyState=t.INIT=0,t.WRITING=1,t.DONE=2,t.error=t.onwritestart=t.onprogress=t.onwrite=t.onabort=t.onerror=t.onwriteend=null,function(t,e,o){return new l(t,e||t.name||"download",o)})}("undefined"!=typeof self&&self||void 0!==C&&C||this.content),y=(t.fileSave=N,function(t){return t.newline||(navigator.userAgent.match(/Windows/)?"\r\n":"\n")});function O(t){for(var e="A".charCodeAt(0),o="Z".charCodeAt(0)-e+1,l="";0<=t;)l=String.fromCharCode(t%o+e)+l,t=Math.floor(t/o)-1;return l}try{var z,E=new XMLSerializer}catch(t){}function A(t,e,o){var l=t.createElement(e);return o&&(o.attr&&S(l).attr(o.attr),o.children&&S.each(o.children,function(t,e){l.appendChild(e)}),null!==o.text)&&void 0!==o.text&&l.appendChild(t.createTextNode(o.text)),l}function D(t,e,o,l,n){var r=S("mergeCells",t);r[0].appendChild(A(t,"mergeCell",{attr:{ref:O(o)+e+":"+O(o+n-1)+(e+l-1)}})),r.attr("count",parseFloat(r.attr("count"))+1)}var R={"_rels/.rels":'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>',"xl/_rels/workbook.xml.rels":'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>',"[Content_Types].xml":'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="xml" ContentType="application/xml" /><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" /><Default Extension="jpeg" ContentType="image/jpeg" /><Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" /><Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" /><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" /></Types>',"xl/workbook.xml":'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><fileVersion appName="xl" lastEdited="5" lowestEdited="5" rupBuild="24816"/><workbookPr showInkAnnotation="0" autoCompressPictures="0"/><bookViews><workbookView xWindow="0" yWindow="0" windowWidth="25600" windowHeight="19020" tabRatio="500"/></bookViews><sheets><sheet name="Sheet1" sheetId="1" r:id="rId1"/></sheets><definedNames/></workbook>',"xl/worksheets/sheet1.xml":'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x14ac" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac"><sheetData/><mergeCells count="0"/></worksheet>',"xl/styles.xml":'<?xml version="1.0" encoding="UTF-8"?><styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="x14ac" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac"><numFmts count="6"><numFmt numFmtId="164" formatCode="[$$-409]#,##0.00;-[$$-409]#,##0.00"/><numFmt numFmtId="165" formatCode=""£"#,##0.00"/><numFmt numFmtId="166" formatCode="[$€-2] #,##0.00"/><numFmt numFmtId="167" formatCode="0.0%"/><numFmt numFmtId="168" formatCode="#,##0;(#,##0)"/><numFmt numFmtId="169" formatCode="#,##0.00;(#,##0.00)"/></numFmts><fonts count="5" x14ac:knownFonts="1"><font><sz val="11" /><name val="Calibri" /></font><font><sz val="11" /><name val="Calibri" /><color rgb="FFFFFFFF" /></font><font><sz val="11" /><name val="Calibri" /><b /></font><font><sz val="11" /><name val="Calibri" /><i /></font><font><sz val="11" /><name val="Calibri" /><u /></font></fonts><fills count="6"><fill><patternFill patternType="none" /></fill><fill><patternFill patternType="none" /></fill><fill><patternFill patternType="solid"><fgColor rgb="FFD9D9D9" /><bgColor indexed="64" /></patternFill></fill><fill><patternFill patternType="solid"><fgColor rgb="FFD99795" /><bgColor indexed="64" /></patternFill></fill><fill><patternFill patternType="solid"><fgColor rgb="ffc6efce" /><bgColor indexed="64" /></patternFill></fill><fill><patternFill patternType="solid"><fgColor rgb="ffc6cfef" /><bgColor indexed="64" /></patternFill></fill></fills><borders count="2"><border><left /><right /><top /><bottom /><diagonal /></border><border diagonalUp="false" diagonalDown="false"><left style="thin"><color auto="1" /></left><right style="thin"><color auto="1" /></right><top style="thin"><color auto="1" /></top><bottom style="thin"><color auto="1" /></bottom><diagonal /></border></borders><cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" /></cellStyleXfs><cellXfs count="68"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="1" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="2" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="3" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="4" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="0" fillId="2" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="1" fillId="2" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="2" fillId="2" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="3" fillId="2" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="4" fillId="2" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="0" fillId="3" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="1" fillId="3" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="2" fillId="3" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="3" fillId="3" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="4" fillId="3" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="0" fillId="4" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="1" fillId="4" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="2" fillId="4" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="3" fillId="4" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="4" fillId="4" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="0" fillId="5" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="1" fillId="5" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="2" fillId="5" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="3" fillId="5" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="4" fillId="5" borderId="0" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="0" fillId="0" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="1" fillId="0" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="2" fillId="0" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="3" fillId="0" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="4" fillId="0" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="0" fillId="2" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="1" fillId="2" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="2" fillId="2" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="3" fillId="2" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="4" fillId="2" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="0" fillId="3" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="1" fillId="3" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="2" fillId="3" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="3" fillId="3" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="4" fillId="3" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="0" fillId="4" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="1" fillId="4" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="2" fillId="4" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="3" fillId="4" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="4" fillId="4" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="0" fillId="5" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="1" fillId="5" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="2" fillId="5" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="3" fillId="5" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="4" fillId="5" borderId="1" applyFont="1" applyFill="1" applyBorder="1"/><xf numFmtId="0" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyAlignment="1"><alignment horizontal="left"/></xf><xf numFmtId="0" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyAlignment="1"><alignment horizontal="center"/></xf><xf numFmtId="0" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyAlignment="1"><alignment horizontal="right"/></xf><xf numFmtId="0" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyAlignment="1"><alignment horizontal="fill"/></xf><xf numFmtId="0" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyAlignment="1"><alignment textRotation="90"/></xf><xf numFmtId="0" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyAlignment="1"><alignment wrapText="1"/></xf><xf numFmtId="9" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/><xf numFmtId="164" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/><xf numFmtId="165" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/><xf numFmtId="166" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/><xf numFmtId="167" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/><xf numFmtId="168" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/><xf numFmtId="169" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/><xf numFmtId="3" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/><xf numFmtId="4" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/><xf numFmtId="1" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/><xf numFmtId="2" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/><xf numFmtId="14" fontId="0" fillId="0" borderId="0" applyFont="1" applyFill="1" applyBorder="1" xfId="0" applyNumberFormat="1"/></cellXfs><cellStyles count="1"><cellStyle name="Normal" xfId="0" builtinId="0" /></cellStyles><dxfs count="0" /><tableStyles count="0" defaultTableStyle="TableStyleMedium9" defaultPivotStyle="PivotStyleMedium4" /></styleSheet>'},_=[{match:/^\-?\d+\.\d%$/,style:60,fmt:function(t){return t/100}},{match:/^\-?\d+\.?\d*%$/,style:56,fmt:function(t){return t/100}},{match:/^\-?\$[\d,]+.?\d*$/,style:57},{match:/^\-?£[\d,]+.?\d*$/,style:58},{match:/^\-?€[\d,]+.?\d*$/,style:59},{match:/^\-?\d+$/,style:65},{match:/^\-?\d+\.\d{2}$/,style:66},{match:/^\([\d,]+\)$/,style:61,fmt:function(t){return-1*t.replace(/[\(\)]/g,"")}},{match:/^\([\d,]+\.\d{2}\)$/,style:62,fmt:function(t){return-1*t.replace(/[\(\)]/g,"")}},{match:/^\-?[\d,]+$/,style:63},{match:/^\-?[\d,]+\.\d{2}$/,style:64},{match:/^(19\d\d|[2-9]\d\d\d)\-(0\d|1[012])\-[0123][\d]$/,style:67,fmt:function(t){return Math.round(25569+Date.parse(t)/864e5)}}];return t.ext.buttons.copyHtml5={className:"buttons-copy buttons-html5",text:function(t){return t.i18n("buttons.copy","Copy")},action:function(t,e,o,l,n){var r=c(e,l),a=e.buttons.exportInfo(l),d=y(l),p=r.str,i=S("<div/>").css({height:1,width:1,overflow:"hidden",position:"fixed",top:0,left:0}),d=(a.title&&(p=a.title+d+d+p),a.messageTop&&(p=a.messageTop+d+d+p),a.messageBottom&&(p=p+d+d+a.messageBottom),l.customize&&(p=l.customize(p,l,e)),S("<textarea readonly/>").val(p).appendTo(i));if(u.queryCommandSupported("copy")){i.appendTo(e.table().container()),d[0].focus(),d[0].select();try{var f=u.execCommand("copy");if(i.remove(),f)return e.buttons.info(e.i18n("buttons.copyTitle","Copy to clipboard"),e.i18n("buttons.copySuccess",{1:"Copied one row to clipboard",_:"Copied %d rows to clipboard"},r.rows),2e3),void n()}catch(t){}}function m(){s.off("click.buttons-copy"),S(u).off(".buttons-copy"),e.buttons.info(!1)}var a=S("<span>"+e.i18n("buttons.copyKeys","Press <i>ctrl</i> or <i>⌘</i> + <i>C</i> to copy the table data<br>to your system clipboard.<br><br>To cancel, click this message or press escape.")+"</span>").append(i),s=(e.buttons.info(e.i18n("buttons.copyTitle","Copy to clipboard"),a,0),d[0].focus(),d[0].select(),S(a).closest(".dt-button-info"));s.on("click.buttons-copy",m),S(u).on("keydown.buttons-copy",function(t){27===t.keyCode&&(m(),n())}).on("copy.buttons-copy cut.buttons-copy",function(){m(),n()})},async:100,exportOptions:{},fieldSeparator:"\t",fieldBoundary:"",header:!0,footer:!0,title:"*",messageTop:"*",messageBottom:"*"},t.ext.buttons.csvHtml5={bom:!1,className:"buttons-csv buttons-html5",available:function(){return void 0!==C.FileReader&&C.Blob},text:function(t){return t.i18n("buttons.csv","CSV")},action:function(t,e,o,l,n){var r=c(e,l).str,a=e.buttons.exportInfo(l),d=l.charset;l.customize&&(r=l.customize(r,l,e)),d=!1!==d?(d=d||u.characterSet||u.charset)&&";charset="+d:"",l.bom&&(r=String.fromCharCode(65279)+r),N(new Blob([r],{type:"text/csv"+d}),a.filename,!0),n()},async:100,filename:"*",extension:".csv",exportOptions:{},fieldSeparator:",",fieldBoundary:'"',escapeChar:'"',charset:null,header:!0,footer:!0},t.ext.buttons.excelHtml5={className:"buttons-excel buttons-html5",available:function(){return void 0!==C.FileReader&&void 0!==T()&&!s()&&E},text:function(t){return t.i18n("buttons.excel","Excel")},action:function(t,e,o,f,l){function n(t){return t=R[t],S.parseXML(t)}function r(t){s=A(c,"row",{attr:{r:m=u+1}});for(var e=0,o=t.length;e<o;e++){var l=O(e)+""+m,n=null;if(null===t[e]||void 0===t[e]||""===t[e]){if(!0!==f.createEmptyCells)continue;t[e]=""}var r=t[e];t[e]="function"==typeof t[e].trim?t[e].trim():t[e];for(var a=0,d=_.length;a<d;a++){var p=_[a];if(t[e].match&&!t[e].match(/^0\d+/)&&t[e].match(p.match)){var i=t[e].replace(/[^\d\.\-]/g,"");p.fmt&&(i=p.fmt(i)),n=A(c,"c",{attr:{r:l,s:p.style},children:[A(c,"v",{text:i})]});break}}n=n||("number"==typeof t[e]||t[e].match&&t[e].match(/^-?\d+(\.\d+)?([eE]\-?\d+)?$/)&&!t[e].match(/^0\d+/)?A(c,"c",{attr:{t:"n",r:l},children:[A(c,"v",{text:t[e]})]}):(r=r.replace?r.replace(/[\x00-\x09\x0B\x0C\x0E-\x1F\x7F-\x9F]/g,""):r,A(c,"c",{attr:{t:"inlineStr",r:l},children:{row:A(c,"is",{children:{row:A(c,"t",{text:r,attr:{"xml:space":"preserve"}})}})}}))),s.appendChild(n)}y.appendChild(s),u++}function a(t){t.forEach(function(t){r(t.map(function(t){return t?t.title:""})),S("row:last c",c).attr("s","2"),t.forEach(function(t,e){t&&(1<t.colSpan||1<t.rowSpan)&&D(c,u,e,t.rowSpan,t.colSpan)})})}var d,m,s,u=0,c=n("xl/worksheets/sheet1.xml"),y=c.getElementsByTagName("sheetData")[0],p={_rels:{".rels":n("_rels/.rels")},xl:{_rels:{"workbook.xml.rels":n("xl/_rels/workbook.xml.rels")},"workbook.xml":n("xl/workbook.xml"),"styles.xml":n("xl/styles.xml"),worksheets:{"sheet1.xml":c}},"[Content_Types].xml":n("[Content_Types].xml")},i=e.buttons.exportData(f.exportOptions),I=e.buttons.exportInfo(f);I.title&&(r([I.title]),D(c,u,0,1,i.header.length),S("row:last c",c).attr("s","51")),I.messageTop&&(r([I.messageTop]),D(c,u,0,1,i.header.length)),f.header&&a(i.headerStructure);for(var F=u,x=0,h=i.body.length;x<h;x++)r(i.body[x]);d=u,f.footer&&i.footer&&a(i.footerStructure),I.messageBottom&&(r([I.messageBottom]),D(c,u,0,1,i.header.length));var b=A(c,"cols");S("worksheet",c).prepend(b);for(var g=0,v=i.header.length;g<v;g++)b.appendChild(A(c,"col",{attr:{min:g+1,max:g+1,width:function(t,e){var o=t.header[e].length;t.footer&&t.footer[e]&&t.footer[e].length>o&&(o=t.footer[e].length);for(var l=0,n=t.body.length;l<n;l++){var r,a=t.body[l][e];if(40<(o=o<(r=(-1!==(a=null!=a?a.toString():"").indexOf("\n")?((r=a.split("\n")).sort(function(t,e){return e.length-t.length}),r[0]):a).length)?r:o))return 54}return 6<(o*=1.35)?o:6}(i,g),customWidth:1}}));var w=p.xl["workbook.xml"];S("sheets sheet",w).attr("name",k(f)),f.autoFilter&&(S("mergeCells",c).before(A(c,"autoFilter",{attr:{ref:"A"+F+":"+O(i.header.length-1)+d}})),S("definedNames",w).append(A(w,"definedName",{attr:{name:"_xlnm._FilterDatabase",localSheetId:"0",hidden:1},text:"'"+k(f).replace(/'/g,"''")+"'!$A$"+F+":"+O(i.header.length-1)+d}))),f.customize&&f.customize(p,f,e),0===S("mergeCells",c).children().length&&S("mergeCells",c).remove();var w=new(T()),F={compression:"DEFLATE",type:"blob",mimeType:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},B=(!function f(m,t){void 0===z&&(z=-1===E.serializeToString((new C.DOMParser).parseFromString(R["xl/worksheets/sheet1.xml"],"text/xml")).indexOf("xmlns:r")),S.each(t,function(t,e){if(S.isPlainObject(e))f(m.folder(t),e);else{if(z){for(var o,l=e.childNodes[0],n=[],r=l.attributes.length-1;0<=r;r--){var a=l.attributes[r].nodeName,d=l.attributes[r].nodeValue;-1!==a.indexOf(":")&&(n.push({name:a,value:d}),l.removeAttribute(a))}for(r=0,o=n.length;r<o;r++){var p=e.createAttribute(n[r].name.replace(":","_dt_b_namespace_token_"));p.value=n[r].value,l.setAttributeNode(p)}}var i=E.serializeToString(e),i=(i=z?(i=(i=-1===i.indexOf("<?xml")?'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'+i:i).replace(/_dt_b_namespace_token_/g,":")).replace(/xmlns:NS[\d]+="" NS[\d]+:/g,""):i).replace(/<([^<>]*?) xmlns=""([^<>]*?)>/g,"<$1 $2>");m.file(t,i)}})}(w,p),I.filename);175<B&&(B=B.substr(0,175)),f.customizeZip&&f.customizeZip(w,i,B),w.generateAsync?w.generateAsync(F).then(function(t){N(t,B),l()}):(N(w.generate(F),B),l())},async:100,filename:"*",extension:".xlsx",exportOptions:{},header:!0,footer:!0,title:"*",messageTop:"*",messageBottom:"*",createEmptyCells:!1,autoFilter:!1,sheetName:""},t.ext.buttons.pdfHtml5={className:"buttons-pdf buttons-html5",available:function(){return void 0!==C.FileReader&&m()},text:function(t){return t.i18n("buttons.pdf","PDF")},action:function(t,e,o,l,n){var r=e.buttons.exportData(l.exportOptions),a=e.buttons.exportInfo(l),d=[];l.header&&r.headerStructure.forEach(function(t){d.push(t.map(function(t){return t?{text:t.title,colSpan:t.colspan,rowSpan:t.rowspan,style:"tableHeader"}:{}}))});for(var p=0,i=r.body.length;p<i;p++)d.push(r.body[p].map(function(t){return{text:null==t?"":"string"==typeof t?t:t.toString()}}));l.footer&&r.footerStructure.forEach(function(t){d.push(t.map(function(t){return t?{text:t.title,colSpan:t.colspan,rowSpan:t.rowspan,style:"tableHeader"}:{}}))});var f={pageSize:l.pageSize,pageOrientation:l.orientation,content:[{style:"table",table:{headerRows:r.headerStructure.length,footerRows:r.footerStructure.length,body:d},layout:{hLineWidth:function(t,e){return 0===t||t===e.table.body.length?0:.5},vLineWidth:function(){return 0},hLineColor:function(t,e){return t===e.table.headerRows||t===e.table.body.length-e.table.footerRows?"#333":"#ddd"},fillColor:function(t){return t<r.headerStructure.length?"#fff":t%2==0?"#f3f3f3":null},paddingTop:function(){return 5},paddingBottom:function(){return 5}}}],styles:{tableHeader:{bold:!0,fontSize:11,alignment:"center"},tableFooter:{bold:!0,fontSize:11},table:{margin:[0,5,0,5]},title:{alignment:"center",fontSize:13},message:{}},defaultStyle:{fontSize:10}},e=(a.messageTop&&f.content.unshift({text:a.messageTop,style:"message",margin:[0,0,0,12]}),a.messageBottom&&f.content.push({text:a.messageBottom,style:"message",margin:[0,0,0,12]}),a.title&&f.content.unshift({text:a.title,style:"title",margin:[0,0,0,12]}),l.customize&&l.customize(f,l,e),m().createPdf(f));"open"!==l.download||s()?e.download(a.filename):e.open(),n()},async:100,title:"*",filename:"*",extension:".pdf",exportOptions:{},orientation:"portrait",pageSize:"en-US"===navigator.language||"en-CA"===navigator.language?"LETTER":"A4",header:!0,footer:!0,messageTop:"*",messageBottom:"*",customize:null,download:"download"},t});
-
diff --git a/dist/constants.js b/dist/constants.js
index 02b0dd1..5e7d3d2 100644
--- a/dist/constants.js
+++ b/dist/constants.js
@@ -1 +1 @@
-const AppConstants = {"parsers":"columnNameAtom\n extends stringAtom\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n extends stringAtom\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint comment\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n extends anyAtom\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\nscriptAnyAtom\n extends codeAtom\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom keywordAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint string\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\njavascriptAtom\n extends stringAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\"<a \") ? false : true\n return shouldLinkify ? this.replaceNotes(Utils.linkify(originalText)) : originalText\n }\n replaceNotes(originalText) {\n // Skip the replacements if there are no footnotes or the text has none.\n if (!this.root.footnotes.length || !originalText.includes(\"^\")) return originalText\n this.root.footnotes.forEach((note, index) => {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `<a href=\"#${note.htmlId}\" class=\"scrollNoteLink\" id=\"${linkBack}\"><sup>${note.label}</sup></a>`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.text}${this.closingTag}`\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return `</${tag}>`\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `<span class=\"scrollBlink\">${super.buildHtml()}</span>\n <script>setInterval(()=>{ Array.from(document.getElementsByClassName(\"scrollBlink\")).forEach(el => el.style.visibility = el.style.visibility === \"hidden\" ? \"visible\" : \"hidden\") }, 500)</script>`\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"linkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"</center>\")\n return `<center>${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser\n javascript\n compileSubparticles() {\n return this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildHtml() {\n return super.buildHtml() + this.compileSubparticles()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `<div style=\"text-indent:${(this.getIndentLevel() - 1) * 20}px;\"><input type=\"checkbox\" ${this.checked} id=\"${this.id}\"><label for=\"${this.id}\">` + super.text + `</label></div>`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `</${listType}>` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return `<style>\\n.${this.constructor.name} li::marker {content: \"${this.cue} \";}\\n</style>`\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* <span id=\"counter${id}\" title=\"0\">0</span><script>setInterval(()=>{ const el = document.getElementById('counter${id}'); el.title = parseFloat(el.title) + ${increment}; el.textContent = Math.floor(parseFloat(el.title)).toLocaleString()}, 100)</script> ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"</details>\")\n return `<details>${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `<a class=\"scrollFootNoteUsageLink\" href=\"#noteUsage${this.noteDefinitionIndex}\">${this.label}</a> ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"</div>\")\n return `<div class=\"scrollSection\">` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}></${this.tag}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return `<audio controls ${this.getAsHtmlAttributes(\"loop autoplay\".split(\" \"))}><source src=\"${this.filename}\" type=\"audio/mpeg\"></audio>`\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom\n heightParser\n cueFromId\n atoms cueAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\n widthParser\n // todo: fix inheritance bug\n cueFromId\n atoms cueAtom\n heightParser\n cueFromId\n atoms cueAtom\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* <span class=\"scrollStopwatchParser\" id=\"stopwatch${id}\">0.0</span><script>{let startTime = parseFloat(new URLSearchParams(window.location.search).get('start') || 0); document.getElementById('stopwatch${id}').title = startTime; setInterval(()=>{ const el = document.getElementById('stopwatch${id}'); el.title = parseFloat(el.title) + .1; el.textContent = (parseFloat(el.title)).toFixed(1)}, 100)}</script> `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"</div>\") // Single columns are self-closing after section break.\n return stackContents + `<div class=\"scrollColumns\" style=\"column-width:${columnWidth}ch;column-count:${maxColumns};max-width:${maxTotalWidth}ch;\">`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"</div>\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"</div>\")\n return `<style>.scrollContainerParser{width: 100%; box-sizing: border-box; max-width: ${this.maxWidth}; margin: 0 auto;}</style>` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `<div class=\"${this.defaultClass}\"><span>${this.dinkus}</span></div>`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n javascript\n buildHtml() {\n return `<hr>`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `<style>.abstractIconButtonParser {position:absolute;top:0.25rem; }.abstractIconButtonParser svg {fill: rgba(204,204,204,.8);width:1.875rem;height:1.875rem; padding: 0 7px;} .abstractIconButtonParser:hover svg{fill: #333;}</style><a href=\"${this.link}\" class=\"doNotPrint abstractIconButtonParser\" style=\"${this.style}\">${this.svg}</a>`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg <svg fill=\"#000000\" xmlns=\"http://www.w3.org/2000/svg\" width=\"800px\" height=\"800px\" viewBox=\"0 0 52 52\" enable-background=\"new 0 0 52 52\" xml:space=\"preserve\"><path d=\"M38.6,20.4c-1-6.5-6.7-11.5-13.5-11.5c-7.6,0-13.7,6.1-13.7,13.7c0,0.3,0,0.7,0.1,1c-5,0.4-8.9,4.6-8.9,9.6 c0,5.4,4.3,9.7,9.7,9.7h11.5c-0.8-0.8-8.1-8.1-8.1-8.1c-0.4-0.4-0.4-0.9,0-1.3l1.3-1.3c0.4-0.4,0.9-0.4,1.3,0l3.5,3.5 c0.4,0.4,1.1,0.1,1.1-0.4V21.8c0-0.4,0.5-0.9,1-0.9h1.9c0.5,0,0.9,0.4,0.9,0.9v13.4c0,0.6,0.8,0.8,1.1,0.4l3.5-3.5 c0.4-0.4,0.9-0.4,1.3,0l1.3,1.3c0.4,0.4,0.4,0.9,0,1.3L26,42.9h12.3v0c6.1-0.1,11-5.1,11-11.3C49.4,25.5,44.6,20.6,38.6,20.4z\"/></svg><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->\n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg <svg width=\"800px\" height=\"800px\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M21.1213 2.70705C19.9497 1.53548 18.0503 1.53547 16.8787 2.70705L15.1989 4.38685L7.29289 12.2928C7.16473 12.421 7.07382 12.5816 7.02986 12.7574L6.02986 16.7574C5.94466 17.0982 6.04451 17.4587 6.29289 17.707C6.54127 17.9554 6.90176 18.0553 7.24254 17.9701L11.2425 16.9701C11.4184 16.9261 11.5789 16.8352 11.7071 16.707L19.5556 8.85857L21.2929 7.12126C22.4645 5.94969 22.4645 4.05019 21.2929 2.87862L21.1213 2.70705ZM18.2929 4.12126C18.6834 3.73074 19.3166 3.73074 19.7071 4.12126L19.8787 4.29283C20.2692 4.68336 20.2692 5.31653 19.8787 5.70705L18.8622 6.72357L17.3068 5.10738L18.2929 4.12126ZM15.8923 6.52185L17.4477 8.13804L10.4888 15.097L8.37437 15.6256L8.90296 13.5112L15.8923 6.52185ZM4 7.99994C4 7.44766 4.44772 6.99994 5 6.99994H10C10.5523 6.99994 11 6.55223 11 5.99994C11 5.44766 10.5523 4.99994 10 4.99994H5C3.34315 4.99994 2 6.34309 2 7.99994V18.9999C2 20.6568 3.34315 21.9999 5 21.9999H16C17.6569 21.9999 19 20.6568 19 18.9999V13.9999C19 13.4477 18.5523 12.9999 18 12.9999C17.4477 12.9999 17 13.4477 17 13.9999V18.9999C17 19.5522 16.5523 19.9999 16 19.9999H5C4.44772 19.9999 4 19.5522 4 18.9999V7.99994Z\"/></svg>\n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg <svg viewBox=\"3 5 24 20\" width=\"24\" height=\"20\" xmlns=\"http://www.w3.org/2000/svg\"><g transform=\"matrix(1, 0, 0, 1, 0, -289.0625)\"><path style=\"opacity:1;stroke:none;stroke-width:0.49999997;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1\" d=\"M 5 5 C 4.2955948 5 3.6803238 5.3628126 3.3242188 5.9101562 L 14.292969 16.878906 C 14.696939 17.282876 15.303061 17.282876 15.707031 16.878906 L 26.675781 5.9101562 C 26.319676 5.3628126 25.704405 5 25 5 L 5 5 z M 3 8.4140625 L 3 23 C 3 24.108 3.892 25 5 25 L 25 25 C 26.108 25 27 24.108 27 23 L 27 8.4140625 L 17.121094 18.292969 C 15.958108 19.455959 14.041892 19.455959 12.878906 18.292969 L 3 8.4140625 z \" transform=\"translate(0,289.0625)\" id=\"rect4592\"/></g></svg>\n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg <svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12.7166 3.79541C12.2835 3.49716 11.7165 3.49716 11.2834 3.79541L4.14336 8.7121C3.81027 8.94146 3.60747 9.31108 3.59247 9.70797C3.54064 11.0799 3.4857 13.4824 3.63658 15.1877C3.7504 16.4742 4.05336 18.1747 4.29944 19.4256C4.41371 20.0066 4.91937 20.4284 5.52037 20.4284H8.84433C8.98594 20.4284 9.10074 20.3111 9.10074 20.1665V15.9754C9.10074 14.9627 9.90433 14.1417 10.8956 14.1417H13.4091C14.4004 14.1417 15.204 14.9627 15.204 15.9754V20.1665C15.204 20.3111 15.3188 20.4284 15.4604 20.4284H18.4796C19.0806 20.4284 19.5863 20.0066 19.7006 19.4256C19.9466 18.1747 20.2496 16.4742 20.3634 15.1877C20.5143 13.4824 20.4594 11.0799 20.4075 9.70797C20.3925 9.31108 20.1897 8.94146 19.8566 8.7121L12.7166 3.79541ZM10.4235 2.49217C11.3764 1.83602 12.6236 1.83602 13.5765 2.49217L20.7165 7.40886C21.4457 7.91098 21.9104 8.73651 21.9448 9.64736C21.9966 11.0178 22.0564 13.5119 21.8956 15.3292C21.7738 16.7067 21.4561 18.4786 21.2089 19.7353C20.9461 21.0711 19.7924 22.0001 18.4796 22.0001H15.4604C14.4691 22.0001 13.6655 21.1791 13.6655 20.1665V15.9754C13.6655 15.8307 13.5507 15.7134 13.4091 15.7134H10.8956C10.754 15.7134 10.6392 15.8307 10.6392 15.9754V20.1665C10.6392 21.1791 9.83561 22.0001 8.84433 22.0001H5.52037C4.20761 22.0001 3.05389 21.0711 2.79113 19.7353C2.54392 18.4786 2.22624 16.7067 2.10437 15.3292C1.94358 13.5119 2.00338 11.0178 2.05515 9.64736C2.08957 8.73652 2.55427 7.91098 3.28346 7.40886L10.4235 2.49217Z\"/></svg>\n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg <svg xmlns=\"http://www.w3.org/2000/svg\" direction=\"ltr\" width=\"231.52568231268793\" height=\"249.33156169975996\" viewBox=\"297.27169239534896 1273.121785992385 231.52568231268793 249.33156169975996\" stroke-linecap=\"round\" stroke-linejoin=\"round\" data-color-mode=\"dark\" class=\"tl-container tl-theme__force-sRGB tl-theme__dark\" ><defs/><g transform=\"matrix(1, 0, 0, 1, 395.9682, 1413.3618)\" opacity=\"1\"><g transform=\"scale(1)\"><path d=\"M-5.7342,-1.1484 T-5.0182,-4.6536 -2.6672,-12.9768 1.7374,-22.3903 8.4597,-30.0892 16.5455,-35.2796 25.3478,-37.8247 34.4641,-37.2199 43.0676,-33.4606 50.27,-27.1118 55.7861,-19.7972 59.8042,-11.6764 61.6844,-2.3157 60.8049,7.9615 57.3259,17.5954 51.2605,25.5515 41.9451,31.9237 30.6489,36.2084 18.9812,37.6038 8.4915,36.637 -1.3063,34.5174 -10.6869,31.6604 -19.0463,27.0252 -26.0612,20.1562 -31.864,11.1271 -35.2772,1.0066 -35.7963,-9.1933 -35.203,-19.2337 -32.52,-28.3259 -27.3388,-37.4245 -20.2857,-45.7085 -11.787,-53.0395 -2.4314,-58.4864 7.1724,-61.8739 17.5002,-65.6025 28.2859,-68.1034 38.514,-69.1081 48.1844,-69.9134 57.9136,-70.1787 67.4056,-69.5971 76.297,-67.7446 84.5233,-63.97 92.0158,-57.5003 98.1976,-48.5614 102.8839,-38.056 105.9936,-27.6097 107.074,-18.168 107.175,-8.2882 106.6452,1.9768 105.4734,11.7698 104.0279,21.1013 101.7439,31.0445 98.0977,40.6386 92.7822,49.1039 86.2847,57.2337 78.6099,64.7113 69.8339,70.974 61.2863,75.092 52.4283,78.2186 42.5338,80.7945 32.7733,82.6629 22.8568,83.6574 12.2803,83.3538 2.4971,81.4828 -6.3367,78.789 -15.1061,75.8011 -24.259,72.3779 -33.2286,68.991 -41.5707,64.8363 -49.153,59.1909 -55.8375,52.2938 -62.9311,43.9553 -68.4604,34.4882 -71.2761,23.5855 -72.748,12.7452 -73.7673,2.0453 -74.1187,-8.7386 -72.293,-19.3856 -68.7376,-30.4053 -64.12,-39.7852 -59.1825,-47.8931 -53.7695,-56.4512 -48.1998,-65.7676 -43.1389,-74.401 -36.9006,-81.0179 -28.4317,-86.2639 -18.3342,-90.3521 -8.9259,-93.9355 0.1927,-98.7179 9.3551,-103.3537 18.7651,-106.7552 29.7482,-110.3584 40.9287,-113.446 50.87,-115.2231 57.8279,-116.0074 60.4065,-116.0632 A7.8326,7.8326 0 0 1 61.1735,-100.4168 T58.6018,-100.2201 51.1634,-99.3008 41.695,-96.884 32.4044,-94.0187 22.65,-91.382 13.4974,-87.3756 5.0519,-83.1026 -5.0443,-78.6175 -15.7236,-74.4743 -24.6193,-70.2681 -31.274,-62.5903 -36.3827,-53.9362 -41.2833,-46.2905 -46.0679,-38.5161 -50.639,-30.6576 -54.6876,-22.4063 -57.6038,-12.3346 -58.5104,-1.8678 -57.6022,9.4969 -56.0628,20.477 -53.6434,29.0487 -48.8908,36.7517 -42.4075,43.9784 -35.3064,50.6236 -27.3553,55.2062 -17.6884,59.4064 -7.6442,63.6638 2.4427,67.0449 12.7308,69.4843 22.9294,70.2171 33.2547,69.3921 42.4972,67.7384 52.2753,64.8425 61.8649,60.9142 69.5292,56.0828 76.3604,49.5927 82.431,42.3194 87.219,34.619 90.6076,25.5217 92.547,16.0352 93.8703,7.1315 94.4972,-2.5268 94.3672,-13.3681 93.3204,-24.4252 90.5377,-34.1656 85.7321,-43.6441 78.7334,-51.8057 68.7542,-56.0559 57.9857,-57.2713 48.079,-57.0198 38.4535,-56.3204 28.5246,-55.0036 18.6104,-52.4796 9.6402,-49.559 0.7193,-46.2981 -7.1531,-41.3715 -13.708,-34.8957 -19.5435,-26.853 -22.8356,-17.0622 -23.363,-6.5446 -21.9969,3.1439 -17.1086,11.9113 -9.276,18.744 -0.0685,22.6058 9.3636,24.796 19.2422,25.8817 29.052,25.0411 37.9338,21.3502 45.6819,13.8498 49.6675,4.5228 49.2802,-5.1854 45.3277,-14.4455 38.8603,-21.7879 30.5488,-25.7753 21.3356,-24.3975 13.6687,-19.2391 8.8217,-10.9919 6.423,-2.3609 5.7342,1.1484 A5.8481,5.8481 0 0 1 -5.7342,-1.1484 Z\" stroke-linecap=\"round\"/></g></g></svg>\n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return `<div class=\"abstractTextLinkParser\"><a href=\"${this.link}\">${this.text}</a></div>`\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n <script>\n sendFormViaEmail = form => {\n const mailto = new URL(\"mailto:\")\n const params = []\n const { value, title } = form.querySelector('button[type=\"submit\"]')\n params.push(`subject=${encodeURIComponent(value)}`)\n params.push(`to=${encodeURIComponent(title)}`)\n const oneTextarea = form.querySelector('textarea[title=\"oneTextarea\"]')\n const body = oneTextarea ? codeMirrorInstance.getValue() : Array.from(new FormData(form)).map(([name, value]) => `${name} ${value}`).join(\"\\\\n\")\n params.push(`body=${encodeURIComponent(body)}`)\n mailto.search = params.join(\"&\")\n window.open(mailto.href, '_blank')\n }\n </script>\n string style\n <style> .scrollFormParser {\n font-family: \"Gill Sans\", \"Bitstream Vera Sans\", sans-serif;\n }\n .scrollFormParser input , .scrollFormParser textarea{\n padding: 10px;\n margin-bottom: 10px;\n width: 100%;\n box-sizing: border-box;\n } .scrollFormParser label {\n display: block;\n margin-bottom: 5px;\n }\n </style>\n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = `<textarea placeholder=\"${placeholder}\" id=\"${Name}\" name=\"${Name}\" ${IsRequired ? \"required\" : \"\"}></textarea>`\n else\n tag = `<input placeholder=\"${placeholder}\" type=\"${type}\" id=\"${Name}\" name=\"${Name}\" ${IsRequired ? \"required\" : \"\"}>`\n return `<div><label for=\"${Name}\" title=\"${IsRequired ? \"Required\" : \"\"}\">${ucFirst}${IsRequired ? \"*\" : \"\"}:</label>${tag}</div>`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}<form ${isEmail ? \"onsubmit='sendFormViaEmail(this); return false;'\" : ` method='post' action='${formDestination}'`} class=\"scrollFormParser\">${this.inputs}<button value=\"${subject}\" title=\"${formDestination}\" class=\"scrollButton\" type=\"submit\">${callToAction}</button>${this.footer}</form>`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal codeMirror.css scrollLibs.js constants.js\n string requireOnce\n <link rel=\"stylesheet\" href=\"codeMirror.css\">\n <script src=\"scrollLibs.js\"></script>\n <script src=\"constants.js\"></script>\n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `<textarea title=\"oneTextarea\" rows=\"${Math.min(this.root.measures.length * 2, 30)}\" placeholder=\"${this.placeholder}\" id=\"${Name}\" name=\"${Name}\"></textarea>\n <script id=\"${Name}Parsers\" type=\"text/plain\">${this.parsersBundle}</script>\n <script>{\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n let {width, height} = document.getElementById('${Name}').getBoundingClientRect();\n const sp = new Particle(AppConstants.parsers)\n sp.filter(particle => particle.getLine().match(parserRegex)).forEach(part => {\n part.appendLine(\"boolean suggestInAutocomplete false\")\n })\n const customScrollParser = new HandParsersProgram(sp.toString() + document.getElementById(\"${Name}Parsers\").textContent).compileAndReturnRootParser()\n codeMirrorInstance = new ParsersCodeMirrorMode(\"custom\", () => customScrollParser, undefined, CodeMirror).register().fromTextAreaWithAutocomplete(document.getElementById(\"${Name}\"), {\n lineWrapping: false,\n lineNumbers: false\n })\n codeMirrorInstance.setSize(width, height);\n codeMirrorInstance.setValue(\\`${this.value}\\`); }</script>`\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `<a class=\"scrollContinueReadingLink\" href=\"${linkRelativeToCompileTarget}\">Continue reading...</a>`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `<div class=\"scrollSnippetContainer\">${this.makeSnippet(file.file.scrollProgram, buildSettings)}</div>`\n }).join(\"\\n\\n\")\n return `<div class=\"scrollColumns\" style=\"column-width:35ch;\">${snippets}</div>`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue nav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return `<nav class=\"scrollNavParser\">` + this.root.getFilesByTags(this.content).map(file => {\n const { linkTitle, permalink } = file.file.scrollProgram\n return `<a href=\"${permalink}\">${linkTitle}</a>` \n }).join(this.get(\"join\") || \" \") + `</nav>`\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `<div><a href=\"${permalink}\">${title}</a><div>${description}...</div><div class=\"subdued\" style=\"text-align:right;\">${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}</div></div>`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `<div id=\"${id}\" class=\"scrollNoticesParser\" style=\"display:none;\"></div><script>(function(){\n const params = new URLSearchParams(window.location.search)\n if (!params.size) return\n document.getElementById('${id}').innerHTML = Array.from(params.entries()).map(([key, value]) => {\n if (key === \"error\") \n return '<div style=\"color:red\">Error: ' + value + '</div>'\n if (key === \"success\")\n return '<div style=\"color:green\">Success: ' + value + '</div>'\n return '<div>' + key + ': ' + value + '</div>'\n }).join(\"\") || \"No query parameters found\"\n document.getElementById('${id}').style.display = \"block\"\n })()</script>`\n }\nprintSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.buildTxt().replace(/\\</g, \"<\")}</code>`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual} = this\n const expected = this.subparticlesToString()\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType anyAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n javascript\n buildHtml() {\n return `<div class=\"printDateParser\">${this.day}</div>`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne() { await this._buildFileType(this.extension) }\nbuildCsvParser\n popularity 0.000096\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo() {\n await this._buildFileType(this.extension)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(externalFilesCopied) {\n await this._copyExternalFiles(externalFilesCopied)\n await super.buildTwo()\n }\n async _copyExternalFiles(externalFilesCopied = {}) {\n if (!this.isNodeJs()) return\n const {root} = this\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Write RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Write concepts to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom anyAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildCommandParser\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Write measures to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom anyAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo() {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${this.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the <html lang=\"\"> tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Meta tag description.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType anyAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType anyAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `<div style=\"text-align: ${index % 2 ? \"right\" : \"left\"};\" class=\"scrollChat ${index % 2 ? \"scrollChatRight\" : \"scrollChatLeft\"}\"><span>${line.asString}</span></div>` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = typeof d3 === \"undefined\" ? require('d3') : d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = typeof d3 === \"undefined\" ? require('d3') : d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/</g, \"<\")\n return {\n title, titleLink: relativePath + permalink, text, date, wordCount, minutes\n }\n }\n columnNames = \"title titleLink text date wordCount minutes\".split(\" \")\nscrollPostsParser\n popularity 0.000024\n cue posts\n description Posts as table.\n extends abstractPostsParser\n example\n // Print a search table:\n posts\n printTable\n tableSearch\n // Dump a CSV of blog:\n buildHtml\n buildCsv\n buildJson\nscrollPostsMetaParser\n popularity 0.000024\n cue postsMeta\n description Post meta as table.\n extends abstractPostsParser\n javascript\n columnNames = [\"date\", \"year\", \"title\", \"permalink\", \"authors\", \"tags\", \"wordCount\", \"minutes\"]\n postToRow(file) {\n const {date, year, title, permalink, authors, tags, wordCount, minutes} = file.file.scrollProgram\n return {\n date, year, title, permalink, authors, tags, wordCount, minutes\n }\n }\nprintFeedParser\n popularity 0.000048\n description Print group to RSS.\n extends abstractPostsParser\n example\n printFeed index\n printFeed cars/index\n buildRss\n javascript\n buildRss() {\n const {dayjs} = this.root\n const scrollPrograms = this.files.map(file => file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>\n <rss version=\"2.0\">\n <channel>\n <title>${title}</title>\n <link>${baseUrl}</link>\n <description>${description}</description>\n <lastBuildDate>${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}</lastBuildDate>\n <language>en-us</language>\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n </channel>\n </rss>`\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.code.replace(/\\</g, \"<\")}</code>`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `<div class=\"codeWithHeader\"><div class=\"codeHeader\">${this.content}</div>${super.buildHtml()}</div>`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n <script>\n document.addEventListener(\"DOMContentLoaded\", () => document.querySelectorAll(\".scrollCodeBlock\").forEach(block =>\n {\n if (!navigator.clipboard) return\n const button = document.createElement(\"span\")\n button.classList.add(\"scrollCopyButton\")\n block.appendChild(button)\n button.addEventListener(\"click\", async () => {\n await navigator.clipboard.writeText(block.innerText)\n button.classList.add(\"scrollCopiedButton\")\n })\n }\n ))\n </script>\nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = `<style>\n .heatrixContainer {\n margin: auto;\n }.heatrixRow {white-space: nowrap;}\n ${cssId} .heatrixAtom {\n font-family: arial;\n border-radius: 2px;\n border: 1px solid transparent;\n display: inline-block;\n margin: 1px;\n text-align: center;\n vertical-align: middle;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n .heatrixAtom a {\n color: black;\n }\n ${cssId} .heatrixAtom{\n width: ${defaultWidth};\n height: ${defaultHeight};\n font-size: ${fontSize};\n line-height: ${lineHeight};\n }\n </style>`\n const firstRow = this.table[0]\n return (\n `<div class=\"heatrixContainer\" id=\"heatrix${this.uid}\">${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `<div class=\"heatrixRow heatrixRow${rowIndex}\">${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return `<div class=\"heatrixAtom heatrixColumn${columnIndex}${valueClass}\" style=\"${inlineStyle}\"><a title=\"${label}\" ${href}>${label}</a></div>`\n })\n .join(\"\")}</div>`\n })\n .join(\"\\n\") +\n \"</div>\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom anyAtom\n cueFromId\n single\n colorParser\n atoms cueAtom anyAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType anyAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal leaflet.css leaflet.js scrollLibs.js\n string requireOnce\n <link rel=\"stylesheet\" href=\"leaflet.css\">\n <script src=\"leaflet.js\"></script>\n <script src=\"scrollLibs.js\"></script>\n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `<div id=\"${mapId}\" style=\"${style}\"></div>\n <script>\n {\n if (!window.maps) window.maps = {}\n const moveToMyLocation = () => {\n if (!navigator.geolocation) return\n navigator.geolocation.getCurrentPosition((position) => {\n const { latitude, longitude } = position.coords\n window.maps.${mapId}.setView([latitude, longitude])\n L.circleMarker([latitude, longitude], {\n fillOpacity: 0.8,\n radius: 20,\n weight: 4\n })\n .addTo(window.maps.${mapId})\n }, () => {})\n }\n const lat = ${this.get(\"lat\") ?? 37.8}\n const long = ${this.get(\"long\") ?? 4}\n if (${this.has(\"geolocate\")}){\n moveToMyLocation()\n }\n const zoomLevel = ${this.get(\"zoom\") ?? 4}\n const hover = '${this.get(\"hover\") || \"<b>{title}</b><br>{description}\"}'\n const template = ${JSON.stringify(template)}\n const points = ${JSON.stringify((this.parent.coreTable || []).filter(point => point.lat && point.long), undefined, 2)}\n window.maps.${mapId} = L.map(\"map${id}\").setView([lat, long], zoomLevel)\n const map = window.maps.${mapId}\n const tileOptions = {\n \"default\": {\n baseLayer: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',\n attribution: '<a href=\"https://www.openstreetmap.org/\">OpenStreetMap</a> contributors'\n },\n light: {\n baseLayer: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',\n attribution: '<a href=\"https://www.openstreetmap.org/\">OpenStreetMap</a> contributors <a href=\"https://carto.com/ attributions\">CARTO</a>'\n },\n }\n const {baseLayer, attribution} = tileOptions.${this.get(\"tiles\") || \"default\"}\n L.tileLayer(baseLayer, {\n attribution,\n maxZoom: 19\n }).addTo(map);\n points.forEach(point => {\n L.circleMarker([point.lat, point.long], {...template, ...Object.fromEntries(\n Object.entries(point).filter(([key, value]) => value !== null)\n )})\n .addTo(map)\n .bindPopup(new Particle(point).evalTemplateString(hover))\n })\n }\n </script>`\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal d3.js plot.js\n string requireOnce\n <script src=\"d3.js\"></script>\n <script src=\"plot.js\"></script>\n example\n plot\n inScope abstractColumnNameParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `<div id=\"${id}\"></div><script>\n {\n let loadChart = async () => {\n const data = ${this.dataCode}\n const get = (col, index ) => col !== \"undefined\" ? col : (index === undefined ? undefined : Object.keys(data[0])[index])\n document.querySelector(\"#${id}\").append(Plot.plot(${this.plotOptions}))\n }\n loadChart()\n }\n </script>`\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n }`\n }\nscatterplotParser\n extends abstractPlotParser\n description Scatterplot Widget.\n // todo: make copyFromExternal work with inheritance\n string copyFromExternal d3.js plot.js\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true,\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal sparkline.js\n string requireOnce <script src=\"sparkline.js\"></script>\n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return `<span id=\"${id}\"></span><script>new Sparkline(document.getElementById(\"${id}\"), {dotRadius: 0, width: ${width}, height: ${height}, lineColor: \"${lineColor}\", tooltip: (value,index) => ${start} + index + \": \" + value}).draw(${JSON.stringify(columnValues)})</script>`\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `<th>${column.name}</th>\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `<a href=\"${link}\">${text}</a>`\n else if (col.name.endsWith(\"Url\")) tagged = `<a href=\"${content}\">${col.name.replace(\"Url\", \"\")}</a>`\n else if (isUrl) tagged = `<a href=\"${content}\">${text}</a>`\n str += `<td>${tagged}</td>\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `<tr>${this.toRow(row)}</tr>`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `<table id=\"table${this._getUid()}\" class=\"scrollTable\">\n <thead><tr>${this.tableHeader.join(\"\\n\")}</tr></thead>\n <tbody>${this.tableBody}</tbody>\n </table>`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal katex.min.css katex.min.js\n string requireOnce\n <link rel=\"stylesheet\" href=\"katex.min.css\">\n <script defer src=\"katex.min.js\"></script>\n <script>\n document.addEventListener(\"DOMContentLoaded\", () => document.querySelectorAll(\".scrollKatex\").forEach(el =>\n {\n katex.render(el.innerText, el, {\n throwOnError: false\n });\n }\n ))\n </script>\n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `<div class=\"scrollKatex\" id=\"${id}\">${content + this.subparticlesToString()}</div>`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `<style>#helpfulNotFound{margin: 100px 0;}</style><h1 id=\"helpfulNotFound\"></h1><script defer src=\"/helpfulNotFound.js\"></script><script>document.addEventListener(\"DOMContentLoaded\", () => new NotFoundApp('${this.content}'))</script>`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal jquery-3.7.1.min.js slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `<style>html {font-size: var(--scrollBaseFontSize, 28px);} body {margin: auto; width: 500px;}.slideshowNav{text-align: center; margin-bottom:20px; font-size: 24px;color: rgba(204,204,204,.8);} a{text-decoration: none; color: rgba(204,204,204,.8);}</style><script defer src=\"jquery-3.7.1.min.js\"></script><div class=\"slideshowNav\"></div><script defer src=\"slideshow.js\"></script>`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal jquery-3.7.1.min.js datatables.css dayjs.min.js datatables.js tableSearch.js\n string requireOnce\n <script defer src=\"jquery-3.7.1.min.js\"></script>\n <style>.dt-search{font-family: \"SF Pro\", \"Helvetica Neue\", \"Segoe UI\", \"Arial\";}</style>\n <link rel=\"stylesheet\" href=\"datatables.css\">\n <script defer src=\"datatables.js\"></script>\n <script defer src=\"dayjs.min.js\"></script>\n <script defer src=\"tableSearch.js\"></script>\n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>${this.css}</style>`\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCs() {\n return this.css\n }\ninlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractScrollParser\n cueFromId\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return `<style>/* ${this.content} */\\n${this.css}</style>`\n }\n get css() {\n return this.atoms.slice(1).map(filename => this.root.readFile(filename)).join(\"\\n\\n\")\n }\n buildCs() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>html, body { background: ${this.content};}</style>`\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>html, body { color: ${this.content};}</style>`\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return `<style>html, body, h1,h2,h3 { font-family: ${font};}</style>`\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return `<link rel=\"stylesheet\" type=\"text/css\" href=\"${this.filename}\">`\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return `<script src=\"${this.filename}\"></script>`\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `<tr>` + items.map(particle => `<td>${particle.cue}<span>${particle.content}</span></td>`).join(\"\\n\") + `</tr>\\n`\n }\n buildHtml() {\n return `<table class=\"scrollDashboard\">${this.tableBody}</table>`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = this[method]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = next[method]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.code.replace(/\\</g, \"<\")}</code>`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType anyAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\ninspectBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal inspector.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `<div class=\"inspectorParticle\"><span class=\"inspectorParticleId\">${particle.constructor.name}</span>${particle.atoms.map((atom, index) => `<span class=\"inspectorAtom\">${atom}<span class=\"inspectorAtomType\">${atomTypes[index]}</span></span>`).join(\" \")}${(particle.length ? `<br><div class=\"inspectorSubparticles\">` + particle.map(mapFn).join(\"<br>\") + `</div>` : \"\")}</div>`}\n return this.selectedParticles.map(mapFn).join(\"<br>\")\n }\n buildHtml() {\n return `<link rel=\"stylesheet\" href=\"inspector.css\">` + this.code\n }\ninspectAboveParser\n description Inspect particle above.\n extends inspectBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return `<style>${this.css}</style>`\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCs() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(`</${tag}>`)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `<br>`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => `<iframe src=\"${url}\" frameborder=\"0\"></iframe>`).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `<figcaption>${caption.buildHtml()}</figcaption>` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `<figure class=\"scrollCaptionedFigure\" style=\"${widthStyle + float}\">${this.getFigureContent(buildSettings)}${captionFig}</figure>`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser linkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"linkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return `<a href=\"${clickLink}\" target=\"${target}\" ${className} ${id}><img src=\"${linkRelativeToCompileTarget}\" ${dimensionAttributes}loading=\"lazy\"></a>`\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \"qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `<div class=\"scrollYouTubeHolder\"><iframe class=\"scrollYouTubeEmbed\" src=\"${url}\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe></div>`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\ninlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractScrollParser\n cueFromId\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return `<script>/* ${this.content} */\\n${this.contents}</script>`\n }\n get contents() {\n return this.atoms.slice(1).map(filename => this.root.readFile(filename)).join(\";\\n\\n\")\n }\n buildJs() {\n return this.contents\n }\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType scriptAnyAtom\n javascript\n buildHtml() {\n return `<script>${this.scriptContent}</script>`\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return `<script>window.${varName} = ${this.root.readFile(this.filename)}</script>`\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<style>${style}</style><a class=\"keyboardNav doNotPrint\" style=\"left:.5rem;\" href=\"${linkToPrevious}\"><</a><a class=\"keyboardNav doNotPrint\" style=\"right:.5rem;\" href=\"${linkToNext}\">></a>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = `<script>document.addEventListener('keydown', function(event) {\n if (document.activeElement !== document.body) return\n if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return // dont interfere with keyboard back button shortcut\n const getLinks = () => document.getElementsByClassName(\"scrollKeyboardNav\")[0].getElementsByTagName(\"a\")\n if (event.key === \"ArrowLeft\")\n getLinks()[0].click()\n else if (event.key === \"ArrowRight\")\n getLinks()[1].click()\n });</script>`\n return `<div class=\"scrollKeyboardNav\" style=\"display:none;\"><a href=\"${linkToPrevious}\">${linkToPrevious}</a> · ${root.permalink} · <a href=\"${linkToNext}\">${linkToNext}</a>${script}</div>`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"<h></h>\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `<style>${css}</style><div id=\"scrollLeetSheet\">` + doc.map(obj => `<div class=\"${obj.category}\"><a href=\"${this.makeLink(obj.examples, obj.id)}\">${obj.isPopular ? \"<b>\" : \"\"}${obj.id}</a> ${obj.description}${obj.isPopular ? \"</b>\" : \"\"}${obj.note}</div>`).join(\"\\n\") + \"</div>\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n buildCsv() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category} = obj\n return {\n id,\n isPopular,\n description,\n popularity,\n category\n }\n })\n return new Particle(this.root.lodash.sortBy(rows, \"isPopular\")).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"<p><b>Parser Definition Parsers</b> define parsers that acquire, analyze and act on code.</p>\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"<p><b>Atom Definition Parsers</b> analyze the atoms in a line.</p>\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` <span class=\"note\">A${category.replace(\"Phase\", \"\").substr(1)}Time.</span>`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `<link rel=\"icon\" href=\"${favicon}\">` : \"\"\n const rssTag = rssFeedUrl ? `<link rel=\"alternate\" type=\"application/rss+xml\" title=\"${title}\" href=\"${rssFeedUrl}\">` : \"\"\n const gitTag = gitRepo ? `<link rel=\"source\" type=\"application/git\" title=\"Source Code Repository\" href=\"${gitRepo}\">` : \"\"\n return `<head>\n <meta charset=\"utf-8\">\n <title>${title}</title>\n <script>/* This HTML was generated by 📜 Scroll v${scrollVersion}. https://scroll.pub */</script>\n <style>@media print {.doNotPrint {display: none !important;}}</style>\n <link rel=\"canonical\" href=\"${canonicalUrl}\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <meta name=\"description\" content=\"${description}\">\n <meta name=\"generator\" content=\"Scroll v${scrollVersion}\">\n <meta property=\"og:title\" content=\"${title}\">\n <meta property=\"og:description\" content=\"${description}\">\n <meta property=\"og:image\" content=\"${openGraphImage}\">\n ${faviconTag}\n ${gitTag}\n ${rssTag}\n <meta name=\"twitter:card\" content=\"summary_large_image\">\n </head>\n <body>`\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `<blockquote class=\"scrollQuote\">${this.subparticlesToString()}</blockquote>`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return `<meta http-equiv=\"Refresh\" content=\"0; url='${this.getAtom(1)}'\" />`\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `<pre>${this.buildTxt()}</pre>`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n <script src=\"scripts/nested/hello.js\"></script>\n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `${name}.css`)\n }\n buildHtml() {\n return this.files.map(name => `<link rel=\"stylesheet\" type=\"text/css\" href=\"${name}\">`).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType anyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `</${this.closeTag}>` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nlinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an <a> tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cueFromId\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get attributes() {\n const attrs = [`href=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends linkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends linkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends linkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `<span class=\"scrollDateline\">${day} — </span>` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: `</${tag}>`, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom anyAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nchatLineParser\n popularity 0.009887\n catchAllAtomType anyAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(reduceParticle => {\n return {\n source: reduceParticle.getAtom(1),\n reduction: reduceParticle.getAtom(2),\n name: reduceParticle.getAtom(3) || reduceParticle.getAtomsFrom(1).join(\"_\")\n }\n })\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n if (reduction === \"concat\") {\n newRow[col.name] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[col.name] = group[0][sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[col.name] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom atomAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.getAtom(1)\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || untypedScalarValue === undefined) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return atom === \"\" || atom === undefined\n else if (operator === \"notEmpty\") return !(atom === \"\" || atom === undefined)\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n return this.getAtomsFrom(1)\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType stringAtom\n cue compose\n example\n table\n compose My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n catchAllAtomType stringAtom\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`<a href=\"${value.includes(\"@\") ? \"mailto:\" : \"\"}${value}\">${name}</a>`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.getAtom(1)\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom atomAtom\n catchAllAtomType atomAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType anyAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nscrollFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType scriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType anyAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nscrollFillParser\n cue fill\n extends abstractColumnNameParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nquoteLineParser\n popularity 0.004172\n catchAllAtomType anyAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return localStorage.getItem(fileOrUrl) || \"\"\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = localStorage.getItem(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n localStorage.setItem(url, await blob.text())\n return localStorage.getItem(url)\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `<a href=\"${this.editUrl}\" class=\"abstractTextLinkParser\">Edit</a>`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"161.3.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particle[methodName])\n .map((particle, index) => particle[methodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"<body>\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `<!DOCTYPE html>\\n<html lang=\"${this.lang}\">\\n${bodyTag}${content}\\n</body>\\n</html>` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll() {\n await this.load()\n await this.buildOne()\n await this.buildTwo()\n }\n async buildOne() {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne()\n }\n }\n async buildTwo(externalFilesCopied = {}) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(externalFilesCopied)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concept[measure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) row[measureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else row[measureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCache[measureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCache[measureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCache[measureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|footer$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"footer\")) {\n const pushes = particle.getParticles(\"footer\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` <item>\n <title>${title}</title>\n <link>${canonicalUrl}</link>\n <pubDate>${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}</pubDate>\n </item>`\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType anyAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n description Table from inline delimited data.\n catchAllAtomType anyAtom\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
\ No newline at end of file
+const AppConstants = {"parsers":"columnNameAtom\n extends stringAtom\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n extends stringAtom\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint comment\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n extends anyAtom\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\nscriptAnyAtom\n extends codeAtom\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom keywordAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint string\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\njavascriptAtom\n extends stringAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\"<a \") ? false : true\n return shouldLinkify ? this.replaceNotes(Utils.linkify(originalText)) : originalText\n }\n replaceNotes(originalText) {\n // Skip the replacements if there are no footnotes or the text has none.\n if (!this.root.footnotes.length || !originalText.includes(\"^\")) return originalText\n this.root.footnotes.forEach((note, index) => {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `<a href=\"#${note.htmlId}\" class=\"scrollNoteLink\" id=\"${linkBack}\"><sup>${note.label}</sup></a>`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.text}${this.closingTag}`\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return `</${tag}>`\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `<span class=\"scrollBlink\">${super.buildHtml()}</span>\n <script>setInterval(()=>{ Array.from(document.getElementsByClassName(\"scrollBlink\")).forEach(el => el.style.visibility = el.style.visibility === \"hidden\" ? \"visible\" : \"hidden\") }, 500)</script>`\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"linkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"</center>\")\n return `<center>${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser\n javascript\n compileSubparticles() {\n return this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildHtml() {\n return super.buildHtml() + this.compileSubparticles()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `<div style=\"text-indent:${(this.getIndentLevel() - 1) * 20}px;\"><input type=\"checkbox\" ${this.checked} id=\"${this.id}\"><label for=\"${this.id}\">` + super.text + `</label></div>`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `</${listType}>` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return `<style>\\n.${this.constructor.name} li::marker {content: \"${this.cue} \";}\\n</style>`\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* <span id=\"counter${id}\" title=\"0\">0</span><script>setInterval(()=>{ const el = document.getElementById('counter${id}'); el.title = parseFloat(el.title) + ${increment}; el.textContent = Math.floor(parseFloat(el.title)).toLocaleString()}, 100)</script> ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"</details>\")\n return `<details>${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `<a class=\"scrollFootNoteUsageLink\" href=\"#noteUsage${this.noteDefinitionIndex}\">${this.label}</a> ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"</div>\")\n return `<div class=\"scrollSection\">` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}></${this.tag}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return `<audio controls ${this.getAsHtmlAttributes(\"loop autoplay\".split(\" \"))}><source src=\"${this.filename}\" type=\"audio/mpeg\"></audio>`\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom\n heightParser\n cueFromId\n atoms cueAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\n widthParser\n // todo: fix inheritance bug\n cueFromId\n atoms cueAtom\n heightParser\n cueFromId\n atoms cueAtom\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* <span class=\"scrollStopwatchParser\" id=\"stopwatch${id}\">0.0</span><script>{let startTime = parseFloat(new URLSearchParams(window.location.search).get('start') || 0); document.getElementById('stopwatch${id}').title = startTime; setInterval(()=>{ const el = document.getElementById('stopwatch${id}'); el.title = parseFloat(el.title) + .1; el.textContent = (parseFloat(el.title)).toFixed(1)}, 100)}</script> `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"</div>\") // Single columns are self-closing after section break.\n return stackContents + `<div class=\"scrollColumns\" style=\"column-width:${columnWidth}ch;column-count:${maxColumns};max-width:${maxTotalWidth}ch;\">`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"</div>\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"</div>\")\n return `<style>.scrollContainerParser{width: 100%; box-sizing: border-box; max-width: ${this.maxWidth}; margin: 0 auto;}</style>` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `<div class=\"${this.defaultClass}\"><span>${this.dinkus}</span></div>`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n javascript\n buildHtml() {\n return `<hr>`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `<style>.abstractIconButtonParser {position:absolute;top:0.25rem; }.abstractIconButtonParser svg {fill: rgba(204,204,204,.8);width:1.875rem;height:1.875rem; padding: 0 7px;} .abstractIconButtonParser:hover svg{fill: #333;}</style><a href=\"${this.link}\" class=\"doNotPrint abstractIconButtonParser\" style=\"${this.style}\">${this.svg}</a>`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg <svg fill=\"#000000\" xmlns=\"http://www.w3.org/2000/svg\" width=\"800px\" height=\"800px\" viewBox=\"0 0 52 52\" enable-background=\"new 0 0 52 52\" xml:space=\"preserve\"><path d=\"M38.6,20.4c-1-6.5-6.7-11.5-13.5-11.5c-7.6,0-13.7,6.1-13.7,13.7c0,0.3,0,0.7,0.1,1c-5,0.4-8.9,4.6-8.9,9.6 c0,5.4,4.3,9.7,9.7,9.7h11.5c-0.8-0.8-8.1-8.1-8.1-8.1c-0.4-0.4-0.4-0.9,0-1.3l1.3-1.3c0.4-0.4,0.9-0.4,1.3,0l3.5,3.5 c0.4,0.4,1.1,0.1,1.1-0.4V21.8c0-0.4,0.5-0.9,1-0.9h1.9c0.5,0,0.9,0.4,0.9,0.9v13.4c0,0.6,0.8,0.8,1.1,0.4l3.5-3.5 c0.4-0.4,0.9-0.4,1.3,0l1.3,1.3c0.4,0.4,0.4,0.9,0,1.3L26,42.9h12.3v0c6.1-0.1,11-5.1,11-11.3C49.4,25.5,44.6,20.6,38.6,20.4z\"/></svg><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->\n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg <svg width=\"800px\" height=\"800px\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M21.1213 2.70705C19.9497 1.53548 18.0503 1.53547 16.8787 2.70705L15.1989 4.38685L7.29289 12.2928C7.16473 12.421 7.07382 12.5816 7.02986 12.7574L6.02986 16.7574C5.94466 17.0982 6.04451 17.4587 6.29289 17.707C6.54127 17.9554 6.90176 18.0553 7.24254 17.9701L11.2425 16.9701C11.4184 16.9261 11.5789 16.8352 11.7071 16.707L19.5556 8.85857L21.2929 7.12126C22.4645 5.94969 22.4645 4.05019 21.2929 2.87862L21.1213 2.70705ZM18.2929 4.12126C18.6834 3.73074 19.3166 3.73074 19.7071 4.12126L19.8787 4.29283C20.2692 4.68336 20.2692 5.31653 19.8787 5.70705L18.8622 6.72357L17.3068 5.10738L18.2929 4.12126ZM15.8923 6.52185L17.4477 8.13804L10.4888 15.097L8.37437 15.6256L8.90296 13.5112L15.8923 6.52185ZM4 7.99994C4 7.44766 4.44772 6.99994 5 6.99994H10C10.5523 6.99994 11 6.55223 11 5.99994C11 5.44766 10.5523 4.99994 10 4.99994H5C3.34315 4.99994 2 6.34309 2 7.99994V18.9999C2 20.6568 3.34315 21.9999 5 21.9999H16C17.6569 21.9999 19 20.6568 19 18.9999V13.9999C19 13.4477 18.5523 12.9999 18 12.9999C17.4477 12.9999 17 13.4477 17 13.9999V18.9999C17 19.5522 16.5523 19.9999 16 19.9999H5C4.44772 19.9999 4 19.5522 4 18.9999V7.99994Z\"/></svg>\n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg <svg viewBox=\"3 5 24 20\" width=\"24\" height=\"20\" xmlns=\"http://www.w3.org/2000/svg\"><g transform=\"matrix(1, 0, 0, 1, 0, -289.0625)\"><path style=\"opacity:1;stroke:none;stroke-width:0.49999997;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1\" d=\"M 5 5 C 4.2955948 5 3.6803238 5.3628126 3.3242188 5.9101562 L 14.292969 16.878906 C 14.696939 17.282876 15.303061 17.282876 15.707031 16.878906 L 26.675781 5.9101562 C 26.319676 5.3628126 25.704405 5 25 5 L 5 5 z M 3 8.4140625 L 3 23 C 3 24.108 3.892 25 5 25 L 25 25 C 26.108 25 27 24.108 27 23 L 27 8.4140625 L 17.121094 18.292969 C 15.958108 19.455959 14.041892 19.455959 12.878906 18.292969 L 3 8.4140625 z \" transform=\"translate(0,289.0625)\" id=\"rect4592\"/></g></svg>\n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg <svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12.7166 3.79541C12.2835 3.49716 11.7165 3.49716 11.2834 3.79541L4.14336 8.7121C3.81027 8.94146 3.60747 9.31108 3.59247 9.70797C3.54064 11.0799 3.4857 13.4824 3.63658 15.1877C3.7504 16.4742 4.05336 18.1747 4.29944 19.4256C4.41371 20.0066 4.91937 20.4284 5.52037 20.4284H8.84433C8.98594 20.4284 9.10074 20.3111 9.10074 20.1665V15.9754C9.10074 14.9627 9.90433 14.1417 10.8956 14.1417H13.4091C14.4004 14.1417 15.204 14.9627 15.204 15.9754V20.1665C15.204 20.3111 15.3188 20.4284 15.4604 20.4284H18.4796C19.0806 20.4284 19.5863 20.0066 19.7006 19.4256C19.9466 18.1747 20.2496 16.4742 20.3634 15.1877C20.5143 13.4824 20.4594 11.0799 20.4075 9.70797C20.3925 9.31108 20.1897 8.94146 19.8566 8.7121L12.7166 3.79541ZM10.4235 2.49217C11.3764 1.83602 12.6236 1.83602 13.5765 2.49217L20.7165 7.40886C21.4457 7.91098 21.9104 8.73651 21.9448 9.64736C21.9966 11.0178 22.0564 13.5119 21.8956 15.3292C21.7738 16.7067 21.4561 18.4786 21.2089 19.7353C20.9461 21.0711 19.7924 22.0001 18.4796 22.0001H15.4604C14.4691 22.0001 13.6655 21.1791 13.6655 20.1665V15.9754C13.6655 15.8307 13.5507 15.7134 13.4091 15.7134H10.8956C10.754 15.7134 10.6392 15.8307 10.6392 15.9754V20.1665C10.6392 21.1791 9.83561 22.0001 8.84433 22.0001H5.52037C4.20761 22.0001 3.05389 21.0711 2.79113 19.7353C2.54392 18.4786 2.22624 16.7067 2.10437 15.3292C1.94358 13.5119 2.00338 11.0178 2.05515 9.64736C2.08957 8.73652 2.55427 7.91098 3.28346 7.40886L10.4235 2.49217Z\"/></svg>\n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg <svg xmlns=\"http://www.w3.org/2000/svg\" direction=\"ltr\" width=\"231.52568231268793\" height=\"249.33156169975996\" viewBox=\"297.27169239534896 1273.121785992385 231.52568231268793 249.33156169975996\" stroke-linecap=\"round\" stroke-linejoin=\"round\" data-color-mode=\"dark\" class=\"tl-container tl-theme__force-sRGB tl-theme__dark\" ><defs/><g transform=\"matrix(1, 0, 0, 1, 395.9682, 1413.3618)\" opacity=\"1\"><g transform=\"scale(1)\"><path d=\"M-5.7342,-1.1484 T-5.0182,-4.6536 -2.6672,-12.9768 1.7374,-22.3903 8.4597,-30.0892 16.5455,-35.2796 25.3478,-37.8247 34.4641,-37.2199 43.0676,-33.4606 50.27,-27.1118 55.7861,-19.7972 59.8042,-11.6764 61.6844,-2.3157 60.8049,7.9615 57.3259,17.5954 51.2605,25.5515 41.9451,31.9237 30.6489,36.2084 18.9812,37.6038 8.4915,36.637 -1.3063,34.5174 -10.6869,31.6604 -19.0463,27.0252 -26.0612,20.1562 -31.864,11.1271 -35.2772,1.0066 -35.7963,-9.1933 -35.203,-19.2337 -32.52,-28.3259 -27.3388,-37.4245 -20.2857,-45.7085 -11.787,-53.0395 -2.4314,-58.4864 7.1724,-61.8739 17.5002,-65.6025 28.2859,-68.1034 38.514,-69.1081 48.1844,-69.9134 57.9136,-70.1787 67.4056,-69.5971 76.297,-67.7446 84.5233,-63.97 92.0158,-57.5003 98.1976,-48.5614 102.8839,-38.056 105.9936,-27.6097 107.074,-18.168 107.175,-8.2882 106.6452,1.9768 105.4734,11.7698 104.0279,21.1013 101.7439,31.0445 98.0977,40.6386 92.7822,49.1039 86.2847,57.2337 78.6099,64.7113 69.8339,70.974 61.2863,75.092 52.4283,78.2186 42.5338,80.7945 32.7733,82.6629 22.8568,83.6574 12.2803,83.3538 2.4971,81.4828 -6.3367,78.789 -15.1061,75.8011 -24.259,72.3779 -33.2286,68.991 -41.5707,64.8363 -49.153,59.1909 -55.8375,52.2938 -62.9311,43.9553 -68.4604,34.4882 -71.2761,23.5855 -72.748,12.7452 -73.7673,2.0453 -74.1187,-8.7386 -72.293,-19.3856 -68.7376,-30.4053 -64.12,-39.7852 -59.1825,-47.8931 -53.7695,-56.4512 -48.1998,-65.7676 -43.1389,-74.401 -36.9006,-81.0179 -28.4317,-86.2639 -18.3342,-90.3521 -8.9259,-93.9355 0.1927,-98.7179 9.3551,-103.3537 18.7651,-106.7552 29.7482,-110.3584 40.9287,-113.446 50.87,-115.2231 57.8279,-116.0074 60.4065,-116.0632 A7.8326,7.8326 0 0 1 61.1735,-100.4168 T58.6018,-100.2201 51.1634,-99.3008 41.695,-96.884 32.4044,-94.0187 22.65,-91.382 13.4974,-87.3756 5.0519,-83.1026 -5.0443,-78.6175 -15.7236,-74.4743 -24.6193,-70.2681 -31.274,-62.5903 -36.3827,-53.9362 -41.2833,-46.2905 -46.0679,-38.5161 -50.639,-30.6576 -54.6876,-22.4063 -57.6038,-12.3346 -58.5104,-1.8678 -57.6022,9.4969 -56.0628,20.477 -53.6434,29.0487 -48.8908,36.7517 -42.4075,43.9784 -35.3064,50.6236 -27.3553,55.2062 -17.6884,59.4064 -7.6442,63.6638 2.4427,67.0449 12.7308,69.4843 22.9294,70.2171 33.2547,69.3921 42.4972,67.7384 52.2753,64.8425 61.8649,60.9142 69.5292,56.0828 76.3604,49.5927 82.431,42.3194 87.219,34.619 90.6076,25.5217 92.547,16.0352 93.8703,7.1315 94.4972,-2.5268 94.3672,-13.3681 93.3204,-24.4252 90.5377,-34.1656 85.7321,-43.6441 78.7334,-51.8057 68.7542,-56.0559 57.9857,-57.2713 48.079,-57.0198 38.4535,-56.3204 28.5246,-55.0036 18.6104,-52.4796 9.6402,-49.559 0.7193,-46.2981 -7.1531,-41.3715 -13.708,-34.8957 -19.5435,-26.853 -22.8356,-17.0622 -23.363,-6.5446 -21.9969,3.1439 -17.1086,11.9113 -9.276,18.744 -0.0685,22.6058 9.3636,24.796 19.2422,25.8817 29.052,25.0411 37.9338,21.3502 45.6819,13.8498 49.6675,4.5228 49.2802,-5.1854 45.3277,-14.4455 38.8603,-21.7879 30.5488,-25.7753 21.3356,-24.3975 13.6687,-19.2391 8.8217,-10.9919 6.423,-2.3609 5.7342,1.1484 A5.8481,5.8481 0 0 1 -5.7342,-1.1484 Z\" stroke-linecap=\"round\"/></g></g></svg>\n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return `<div class=\"abstractTextLinkParser\"><a href=\"${this.link}\">${this.text}</a></div>`\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n <script>\n sendFormViaEmail = form => {\n const mailto = new URL(\"mailto:\")\n const params = []\n const { value, title } = form.querySelector('button[type=\"submit\"]')\n params.push(`subject=${encodeURIComponent(value)}`)\n params.push(`to=${encodeURIComponent(title)}`)\n const oneTextarea = form.querySelector('textarea[title=\"oneTextarea\"]')\n const body = oneTextarea ? codeMirrorInstance.getValue() : Array.from(new FormData(form)).map(([name, value]) => `${name} ${value}`).join(\"\\\\n\")\n params.push(`body=${encodeURIComponent(body)}`)\n mailto.search = params.join(\"&\")\n window.open(mailto.href, '_blank')\n }\n </script>\n string style\n <style> .scrollFormParser {\n font-family: \"Gill Sans\", \"Bitstream Vera Sans\", sans-serif;\n }\n .scrollFormParser input , .scrollFormParser textarea{\n padding: 10px;\n margin-bottom: 10px;\n width: 100%;\n box-sizing: border-box;\n } .scrollFormParser label {\n display: block;\n margin-bottom: 5px;\n }\n </style>\n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = `<textarea placeholder=\"${placeholder}\" id=\"${Name}\" name=\"${Name}\" ${IsRequired ? \"required\" : \"\"}></textarea>`\n else\n tag = `<input placeholder=\"${placeholder}\" type=\"${type}\" id=\"${Name}\" name=\"${Name}\" ${IsRequired ? \"required\" : \"\"}>`\n return `<div><label for=\"${Name}\" title=\"${IsRequired ? \"Required\" : \"\"}\">${ucFirst}${IsRequired ? \"*\" : \"\"}:</label>${tag}</div>`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}<form ${isEmail ? \"onsubmit='sendFormViaEmail(this); return false;'\" : ` method='post' action='${formDestination}'`} class=\"scrollFormParser\">${this.inputs}<button value=\"${subject}\" title=\"${formDestination}\" class=\"scrollButton\" type=\"submit\">${callToAction}</button>${this.footer}</form>`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js\n string requireOnce\n <link rel=\"stylesheet\" href=\".codeMirror.css\">\n <script src=\".scrollLibs.js\"></script>\n <script src=\".constants.js\"></script>\n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `<textarea title=\"oneTextarea\" rows=\"${Math.min(this.root.measures.length * 2, 30)}\" placeholder=\"${this.placeholder}\" id=\"${Name}\" name=\"${Name}\"></textarea>\n <script id=\"${Name}Parsers\" type=\"text/plain\">${this.parsersBundle}</script>\n <script>{\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n let {width, height} = document.getElementById('${Name}').getBoundingClientRect();\n const sp = new Particle(AppConstants.parsers)\n sp.filter(particle => particle.getLine().match(parserRegex)).forEach(part => {\n part.appendLine(\"boolean suggestInAutocomplete false\")\n })\n const customScrollParser = new HandParsersProgram(sp.toString() + document.getElementById(\"${Name}Parsers\").textContent).compileAndReturnRootParser()\n codeMirrorInstance = new ParsersCodeMirrorMode(\"custom\", () => customScrollParser, undefined, CodeMirror).register().fromTextAreaWithAutocomplete(document.getElementById(\"${Name}\"), {\n lineWrapping: false,\n lineNumbers: false\n })\n codeMirrorInstance.setSize(width, height);\n codeMirrorInstance.setValue(\\`${this.value}\\`); }</script>`\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `<a class=\"scrollContinueReadingLink\" href=\"${linkRelativeToCompileTarget}\">Continue reading...</a>`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `<div class=\"scrollSnippetContainer\">${this.makeSnippet(file.file.scrollProgram, buildSettings)}</div>`\n }).join(\"\\n\\n\")\n return `<div class=\"scrollColumns\" style=\"column-width:35ch;\">${snippets}</div>`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue nav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return `<nav class=\"scrollNavParser\">` + this.root.getFilesByTags(this.content).map(file => {\n const { linkTitle, permalink } = file.file.scrollProgram\n return `<a href=\"${permalink}\">${linkTitle}</a>` \n }).join(this.get(\"join\") || \" \") + `</nav>`\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `<div><a href=\"${permalink}\">${title}</a><div>${description}...</div><div class=\"subdued\" style=\"text-align:right;\">${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}</div></div>`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `<div id=\"${id}\" class=\"scrollNoticesParser\" style=\"display:none;\"></div><script>(function(){\n const params = new URLSearchParams(window.location.search)\n if (!params.size) return\n document.getElementById('${id}').innerHTML = Array.from(params.entries()).map(([key, value]) => {\n if (key === \"error\") \n return '<div style=\"color:red\">Error: ' + value + '</div>'\n if (key === \"success\")\n return '<div style=\"color:green\">Success: ' + value + '</div>'\n return '<div>' + key + ': ' + value + '</div>'\n }).join(\"\") || \"No query parameters found\"\n document.getElementById('${id}').style.display = \"block\"\n })()</script>`\n }\nprintSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.buildTxt().replace(/\\</g, \"<\")}</code>`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual} = this\n const expected = this.subparticlesToString()\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType anyAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n javascript\n buildHtml() {\n return `<div class=\"printDateParser\">${this.day}</div>`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne() { await this._buildFileType(this.extension) }\nbuildCsvParser\n popularity 0.000096\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo() {\n await this._buildFileType(this.extension)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(externalFilesCopied) {\n await this._copyExternalFiles(externalFilesCopied)\n await super.buildTwo()\n }\n async _copyExternalFiles(externalFilesCopied = {}) {\n if (!this.isNodeJs()) return\n const {root} = this\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Write RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Write concepts to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom anyAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildCommandParser\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Write measures to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom anyAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo() {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${this.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the <html lang=\"\"> tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Meta tag description.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType anyAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType anyAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `<div style=\"text-align: ${index % 2 ? \"right\" : \"left\"};\" class=\"scrollChat ${index % 2 ? \"scrollChatRight\" : \"scrollChatLeft\"}\"><span>${line.asString}</span></div>` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = typeof d3 === \"undefined\" ? require('d3') : d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = typeof d3 === \"undefined\" ? require('d3') : d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal .clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/</g, \"<\")\n return {\n title, titleLink: relativePath + permalink, text, date, wordCount, minutes\n }\n }\n columnNames = \"title titleLink text date wordCount minutes\".split(\" \")\nscrollPostsParser\n popularity 0.000024\n cue posts\n description Posts as table.\n extends abstractPostsParser\n example\n // Print a search table:\n posts\n printTable\n tableSearch\n // Dump a CSV of blog:\n buildHtml\n buildCsv\n buildJson\nscrollPostsMetaParser\n popularity 0.000024\n cue postsMeta\n description Post meta as table.\n extends abstractPostsParser\n javascript\n columnNames = [\"date\", \"year\", \"title\", \"permalink\", \"authors\", \"tags\", \"wordCount\", \"minutes\"]\n postToRow(file) {\n const {date, year, title, permalink, authors, tags, wordCount, minutes} = file.file.scrollProgram\n return {\n date, year, title, permalink, authors, tags, wordCount, minutes\n }\n }\nprintFeedParser\n popularity 0.000048\n description Print group to RSS.\n extends abstractPostsParser\n example\n printFeed index\n printFeed cars/index\n buildRss\n javascript\n buildRss() {\n const {dayjs} = this.root\n const scrollPrograms = this.files.map(file => file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>\n <rss version=\"2.0\">\n <channel>\n <title>${title}</title>\n <link>${baseUrl}</link>\n <description>${description}</description>\n <lastBuildDate>${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}</lastBuildDate>\n <language>en-us</language>\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n </channel>\n </rss>`\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.code.replace(/\\</g, \"<\")}</code>`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `<div class=\"codeWithHeader\"><div class=\"codeHeader\">${this.content}</div>${super.buildHtml()}</div>`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n <script>\n document.addEventListener(\"DOMContentLoaded\", () => document.querySelectorAll(\".scrollCodeBlock\").forEach(block =>\n {\n if (!navigator.clipboard) return\n const button = document.createElement(\"span\")\n button.classList.add(\"scrollCopyButton\")\n block.appendChild(button)\n button.addEventListener(\"click\", async () => {\n await navigator.clipboard.writeText(block.innerText)\n button.classList.add(\"scrollCopiedButton\")\n })\n }\n ))\n </script>\nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = `<style>\n .heatrixContainer {\n margin: auto;\n }.heatrixRow {white-space: nowrap;}\n ${cssId} .heatrixAtom {\n font-family: arial;\n border-radius: 2px;\n border: 1px solid transparent;\n display: inline-block;\n margin: 1px;\n text-align: center;\n vertical-align: middle;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n .heatrixAtom a {\n color: black;\n }\n ${cssId} .heatrixAtom{\n width: ${defaultWidth};\n height: ${defaultHeight};\n font-size: ${fontSize};\n line-height: ${lineHeight};\n }\n </style>`\n const firstRow = this.table[0]\n return (\n `<div class=\"heatrixContainer\" id=\"heatrix${this.uid}\">${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `<div class=\"heatrixRow heatrixRow${rowIndex}\">${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return `<div class=\"heatrixAtom heatrixColumn${columnIndex}${valueClass}\" style=\"${inlineStyle}\"><a title=\"${label}\" ${href}>${label}</a></div>`\n })\n .join(\"\")}</div>`\n })\n .join(\"\\n\") +\n \"</div>\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom anyAtom\n cueFromId\n single\n colorParser\n atoms cueAtom anyAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType anyAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js\n string requireOnce\n <link rel=\"stylesheet\" href=\".leaflet.css\">\n <script src=\".leaflet.js\"></script>\n <script src=\".scrollLibs.js\"></script>\n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `<div id=\"${mapId}\" style=\"${style}\"></div>\n <script>\n {\n if (!window.maps) window.maps = {}\n const moveToMyLocation = () => {\n if (!navigator.geolocation) return\n navigator.geolocation.getCurrentPosition((position) => {\n const { latitude, longitude } = position.coords\n window.maps.${mapId}.setView([latitude, longitude])\n L.circleMarker([latitude, longitude], {\n fillOpacity: 0.8,\n radius: 20,\n weight: 4\n })\n .addTo(window.maps.${mapId})\n }, () => {})\n }\n const lat = ${this.get(\"lat\") ?? 37.8}\n const long = ${this.get(\"long\") ?? 4}\n if (${this.has(\"geolocate\")}){\n moveToMyLocation()\n }\n const zoomLevel = ${this.get(\"zoom\") ?? 4}\n const hover = '${this.get(\"hover\") || \"<b>{title}</b><br>{description}\"}'\n const template = ${JSON.stringify(template)}\n const points = ${JSON.stringify((this.parent.coreTable || []).filter(point => point.lat && point.long), undefined, 2)}\n window.maps.${mapId} = L.map(\"map${id}\").setView([lat, long], zoomLevel)\n const map = window.maps.${mapId}\n const tileOptions = {\n \"default\": {\n baseLayer: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',\n attribution: '<a href=\"https://www.openstreetmap.org/\">OpenStreetMap</a> contributors'\n },\n light: {\n baseLayer: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',\n attribution: '<a href=\"https://www.openstreetmap.org/\">OpenStreetMap</a> contributors <a href=\"https://carto.com/ attributions\">CARTO</a>'\n },\n }\n const {baseLayer, attribution} = tileOptions.${this.get(\"tiles\") || \"default\"}\n L.tileLayer(baseLayer, {\n attribution,\n maxZoom: 19\n }).addTo(map);\n points.forEach(point => {\n L.circleMarker([point.lat, point.long], {...template, ...Object.fromEntries(\n Object.entries(point).filter(([key, value]) => value !== null)\n )})\n .addTo(map)\n .bindPopup(new Particle(point).evalTemplateString(hover))\n })\n }\n </script>`\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal .d3.js .plot.js\n string requireOnce\n <script src=\".d3.js\"></script>\n <script src=\".plot.js\"></script>\n example\n plot\n inScope abstractColumnNameParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `<div id=\"${id}\"></div><script>\n {\n let loadChart = async () => {\n const data = ${this.dataCode}\n const get = (col, index ) => col !== \"undefined\" ? col : (index === undefined ? undefined : Object.keys(data[0])[index])\n document.querySelector(\"#${id}\").append(Plot.plot(${this.plotOptions}))\n }\n loadChart()\n }\n </script>`\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n }`\n }\nscatterplotParser\n extends abstractPlotParser\n description Scatterplot Widget.\n // todo: make copyFromExternal work with inheritance\n string copyFromExternal .d3.js .plot.js\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true,\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal .sparkline.js\n string requireOnce <script src=\".sparkline.js\"></script>\n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return `<span id=\"${id}\"></span><script>new Sparkline(document.getElementById(\"${id}\"), {dotRadius: 0, width: ${width}, height: ${height}, lineColor: \"${lineColor}\", tooltip: (value,index) => ${start} + index + \": \" + value}).draw(${JSON.stringify(columnValues)})</script>`\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `<th>${column.name}</th>\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `<a href=\"${link}\">${text}</a>`\n else if (col.name.endsWith(\"Url\")) tagged = `<a href=\"${content}\">${col.name.replace(\"Url\", \"\")}</a>`\n else if (isUrl) tagged = `<a href=\"${content}\">${text}</a>`\n str += `<td>${tagged}</td>\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `<tr>${this.toRow(row)}</tr>`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `<table id=\"table${this._getUid()}\" class=\"scrollTable\">\n <thead><tr>${this.tableHeader.join(\"\\n\")}</tr></thead>\n <tbody>${this.tableBody}</tbody>\n </table>`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal .katex.min.css .katex.min.js\n string requireOnce\n <link rel=\"stylesheet\" href=\".katex.min.css\">\n <script defer src=\".katex.min.js\"></script>\n <script>\n document.addEventListener(\"DOMContentLoaded\", () => document.querySelectorAll(\".scrollKatex\").forEach(el =>\n {\n katex.render(el.innerText, el, {\n throwOnError: false\n });\n }\n ))\n </script>\n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `<div class=\"scrollKatex\" id=\"${id}\">${content + this.subparticlesToString()}</div>`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal .helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `<style>#helpfulNotFound{margin: 100px 0;}</style><h1 id=\"helpfulNotFound\"></h1><script defer src=\"/.helpfulNotFound.js\"></script><script>document.addEventListener(\"DOMContentLoaded\", () => new NotFoundApp('${this.content}'))</script>`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `<style>html {font-size: var(--scrollBaseFontSize, 28px);} body {margin: auto; width: 500px;}.slideshowNav{text-align: center; margin-bottom:20px; font-size: 24px;color: rgba(204,204,204,.8);} a{text-decoration: none; color: rgba(204,204,204,.8);}</style><script defer src=\".jquery-3.7.1.min.js\"></script><div class=\"slideshowNav\"></div><script defer src=\".slideshow.js\"></script>`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js\n string requireOnce\n <script defer src=\".jquery-3.7.1.min.js\"></script>\n <style>.dt-search{font-family: \"SF Pro\", \"Helvetica Neue\", \"Segoe UI\", \"Arial\";}</style>\n <link rel=\"stylesheet\" href=\".datatables.css\">\n <script defer src=\".datatables.js\"></script>\n <script defer src=\".dayjs.min.js\"></script>\n <script defer src=\".tableSearch.js\"></script>\n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>${this.css}</style>`\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCs() {\n return this.css\n }\ninlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractScrollParser\n cueFromId\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return `<style>/* ${this.content} */\\n${this.css}</style>`\n }\n get css() {\n return this.atoms.slice(1).map(filename => this.root.readFile(filename)).join(\"\\n\\n\")\n }\n buildCs() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>html, body { background: ${this.content};}</style>`\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>html, body { color: ${this.content};}</style>`\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return `<style>html, body, h1,h2,h3 { font-family: ${font};}</style>`\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return `<link rel=\"stylesheet\" type=\"text/css\" href=\"${this.filename}\">`\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return `<script src=\"${this.filename}\"></script>`\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `<tr>` + items.map(particle => `<td>${particle.cue}<span>${particle.content}</span></td>`).join(\"\\n\") + `</tr>\\n`\n }\n buildHtml() {\n return `<table class=\"scrollDashboard\">${this.tableBody}</table>`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = this[method]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = next[method]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.code.replace(/\\</g, \"<\")}</code>`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType anyAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\ninspectBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal .inspector.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `<div class=\"inspectorParticle\"><span class=\"inspectorParticleId\">${particle.constructor.name}</span>${particle.atoms.map((atom, index) => `<span class=\"inspectorAtom\">${atom}<span class=\"inspectorAtomType\">${atomTypes[index]}</span></span>`).join(\" \")}${(particle.length ? `<br><div class=\"inspectorSubparticles\">` + particle.map(mapFn).join(\"<br>\") + `</div>` : \"\")}</div>`}\n return this.selectedParticles.map(mapFn).join(\"<br>\")\n }\n buildHtml() {\n return `<link rel=\"stylesheet\" href=\".inspector.css\">` + this.code\n }\ninspectAboveParser\n description Inspect particle above.\n extends inspectBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return `<style>${this.css}</style>`\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCs() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(`</${tag}>`)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `<br>`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => `<iframe src=\"${url}\" frameborder=\"0\"></iframe>`).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `<figcaption>${caption.buildHtml()}</figcaption>` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `<figure class=\"scrollCaptionedFigure\" style=\"${widthStyle + float}\">${this.getFigureContent(buildSettings)}${captionFig}</figure>`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser linkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"linkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return `<a href=\"${clickLink}\" target=\"${target}\" ${className} ${id}><img src=\"${linkRelativeToCompileTarget}\" ${dimensionAttributes}loading=\"lazy\"></a>`\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \".qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `<div class=\"scrollYouTubeHolder\"><iframe class=\"scrollYouTubeEmbed\" src=\"${url}\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe></div>`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\ninlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractScrollParser\n cueFromId\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return `<script>/* ${this.content} */\\n${this.contents}</script>`\n }\n get contents() {\n return this.atoms.slice(1).map(filename => this.root.readFile(filename)).join(\";\\n\\n\")\n }\n buildJs() {\n return this.contents\n }\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType scriptAnyAtom\n javascript\n buildHtml() {\n return `<script>${this.scriptContent}</script>`\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return `<script>window.${varName} = ${this.root.readFile(this.filename)}</script>`\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<style>${style}</style><a class=\"keyboardNav doNotPrint\" style=\"left:.5rem;\" href=\"${linkToPrevious}\"><</a><a class=\"keyboardNav doNotPrint\" style=\"right:.5rem;\" href=\"${linkToNext}\">></a>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = `<script>document.addEventListener('keydown', function(event) {\n if (document.activeElement !== document.body) return\n if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return // dont interfere with keyboard back button shortcut\n const getLinks = () => document.getElementsByClassName(\"scrollKeyboardNav\")[0].getElementsByTagName(\"a\")\n if (event.key === \"ArrowLeft\")\n getLinks()[0].click()\n else if (event.key === \"ArrowRight\")\n getLinks()[1].click()\n });</script>`\n return `<div class=\"scrollKeyboardNav\" style=\"display:none;\"><a href=\"${linkToPrevious}\">${linkToPrevious}</a> · ${root.permalink} · <a href=\"${linkToNext}\">${linkToNext}</a>${script}</div>`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"<h></h>\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `<style>${css}</style><div id=\"scrollLeetSheet\">` + doc.map(obj => `<div class=\"${obj.category}\"><a href=\"${this.makeLink(obj.examples, obj.id)}\">${obj.isPopular ? \"<b>\" : \"\"}${obj.id}</a> ${obj.description}${obj.isPopular ? \"</b>\" : \"\"}${obj.note}</div>`).join(\"\\n\") + \"</div>\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n buildCsv() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category} = obj\n return {\n id,\n isPopular,\n description,\n popularity,\n category\n }\n })\n return new Particle(this.root.lodash.sortBy(rows, \"isPopular\")).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"<p><b>Parser Definition Parsers</b> define parsers that acquire, analyze and act on code.</p>\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"<p><b>Atom Definition Parsers</b> analyze the atoms in a line.</p>\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` <span class=\"note\">A${category.replace(\"Phase\", \"\").substr(1)}Time.</span>`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `<link rel=\"icon\" href=\"${favicon}\">` : \"\"\n const rssTag = rssFeedUrl ? `<link rel=\"alternate\" type=\"application/rss+xml\" title=\"${title}\" href=\"${rssFeedUrl}\">` : \"\"\n const gitTag = gitRepo ? `<link rel=\"source\" type=\"application/git\" title=\"Source Code Repository\" href=\"${gitRepo}\">` : \"\"\n return `<head>\n <meta charset=\"utf-8\">\n <title>${title}</title>\n <script>/* This HTML was generated by 📜 Scroll v${scrollVersion}. https://scroll.pub */</script>\n <style>@media print {.doNotPrint {display: none !important;}}</style>\n <link rel=\"canonical\" href=\"${canonicalUrl}\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <meta name=\"description\" content=\"${description}\">\n <meta name=\"generator\" content=\"Scroll v${scrollVersion}\">\n <meta property=\"og:title\" content=\"${title}\">\n <meta property=\"og:description\" content=\"${description}\">\n <meta property=\"og:image\" content=\"${openGraphImage}\">\n ${faviconTag}\n ${gitTag}\n ${rssTag}\n <meta name=\"twitter:card\" content=\"summary_large_image\">\n </head>\n <body>`\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `<blockquote class=\"scrollQuote\">${this.subparticlesToString()}</blockquote>`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return `<meta http-equiv=\"Refresh\" content=\"0; url='${this.getAtom(1)}'\" />`\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `<pre>${this.buildTxt()}</pre>`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n <script src=\"scripts/nested/hello.js\"></script>\n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal .gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `.${name}.css`)\n }\n buildHtml() {\n return this.files.map(name => `<link rel=\"stylesheet\" type=\"text/css\" href=\"${name}\">`).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType anyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `</${this.closeTag}>` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nlinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an <a> tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cueFromId\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get attributes() {\n const attrs = [`href=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends linkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends linkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends linkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `<span class=\"scrollDateline\">${day} — </span>` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: `</${tag}>`, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom anyAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nchatLineParser\n popularity 0.009887\n catchAllAtomType anyAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(reduceParticle => {\n return {\n source: reduceParticle.getAtom(1),\n reduction: reduceParticle.getAtom(2),\n name: reduceParticle.getAtom(3) || reduceParticle.getAtomsFrom(1).join(\"_\")\n }\n })\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n if (reduction === \"concat\") {\n newRow[col.name] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[col.name] = group[0][sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[col.name] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom atomAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.getAtom(1)\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || untypedScalarValue === undefined) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return atom === \"\" || atom === undefined\n else if (operator === \"notEmpty\") return !(atom === \"\" || atom === undefined)\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n return this.getAtomsFrom(1)\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType stringAtom\n cue compose\n example\n table\n compose My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n catchAllAtomType stringAtom\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`<a href=\"${value.includes(\"@\") ? \"mailto:\" : \"\"}${value}\">${name}</a>`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.getAtom(1)\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom atomAtom\n catchAllAtomType atomAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType anyAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nscrollFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType scriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType anyAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nscrollFillParser\n cue fill\n extends abstractColumnNameParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nquoteLineParser\n popularity 0.004172\n catchAllAtomType anyAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return localStorage.getItem(fileOrUrl) || \"\"\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = localStorage.getItem(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n localStorage.setItem(url, await blob.text())\n return localStorage.getItem(url)\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `<a href=\"${this.editUrl}\" class=\"abstractTextLinkParser\">Edit</a>`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"162.0.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particle[methodName])\n .map((particle, index) => particle[methodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"<body>\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `<!DOCTYPE html>\\n<html lang=\"${this.lang}\">\\n${bodyTag}${content}\\n</body>\\n</html>` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll() {\n await this.load()\n await this.buildOne()\n await this.buildTwo()\n }\n async buildOne() {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne()\n }\n }\n async buildTwo(externalFilesCopied = {}) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(externalFilesCopied)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concept[measure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) row[measureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else row[measureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCache[measureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCache[measureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCache[measureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|footer$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"footer\")) {\n const pushes = particle.getParticles(\"footer\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` <item>\n <title>${title}</title>\n <link>${canonicalUrl}</link>\n <pubDate>${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}</pubDate>\n </item>`\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType anyAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n description Table from inline delimited data.\n catchAllAtomType anyAtom\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
\ No newline at end of file
diff --git a/package.json b/package.json
index 7194604..e27a036 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
},
"dependencies": {
"jquery": "^3.6.0",
- "scroll-cli": "^161.3.0",
+ "scroll-cli": "file:../scroll",
"scrollsdk": "^99.2.0"
},
"devDependencies": {
diff --git a/scroll.parsers b/scroll.parsers
index d901a5f..578e78a 100644
--- a/scroll.parsers
+++ b/scroll.parsers
@@ -1092,11 +1092,11 @@ scrollFormParser
cueFromId
single
description Generate a Scroll Form.
- string copyFromExternal codeMirror.css scrollLibs.js constants.js
+ string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js
string requireOnce
- <link rel="stylesheet" href="codeMirror.css">
- <script src="scrollLibs.js"></script>
- <script src="constants.js"></script>
+ <link rel="stylesheet" href=".codeMirror.css">
+ <script src=".scrollLibs.js"></script>
+ <script src=".constants.js"></script>
javascript
get placeholder() {
return this.getParticle("placeholder")?.subparticlesToString() || ""
@@ -1918,7 +1918,7 @@ clocParser
extends scrollTableParser
description Output results of cloc as table.
cue cloc
- string copyFromExternal clocLangs.txt
+ string copyFromExternal .clocLangs.txt
javascript
delimiter = ","
get delimitedData() {
@@ -1928,7 +1928,7 @@ clocParser
return csv
}
get command(){
- return `cloc --vcs git . --csv --read-lang-def=clocLangs.txt ${this.content || ""}`
+ return `cloc --vcs git . --csv --read-lang-def=.clocLangs.txt ${this.content || ""}`
}
scrollDependenciesParser
extends scrollTableParser
@@ -2483,11 +2483,11 @@ mapParser
single
extends abstractTableVisualizationParser
description Map widget.
- string copyFromExternal leaflet.css leaflet.js scrollLibs.js
+ string copyFromExternal .leaflet.css .leaflet.js .scrollLibs.js
string requireOnce
- <link rel="stylesheet" href="leaflet.css">
- <script src="leaflet.js"></script>
- <script src="scrollLibs.js"></script>
+ <link rel="stylesheet" href=".leaflet.css">
+ <script src=".leaflet.js"></script>
+ <script src=".scrollLibs.js"></script>
javascript
buildInstance() {
const height = this.get("height") || 500
@@ -2556,10 +2556,10 @@ mapParser
abstractPlotParser
// Observablehq
extends abstractTableVisualizationParser
- string copyFromExternal d3.js plot.js
+ string copyFromExternal .d3.js .plot.js
string requireOnce
- <script src="d3.js"></script>
- <script src="plot.js"></script>
+ <script src=".d3.js"></script>
+ <script src=".plot.js"></script>
example
plot
inScope abstractColumnNameParser
@@ -2600,7 +2600,7 @@ scatterplotParser
extends abstractPlotParser
description Scatterplot Widget.
// todo: make copyFromExternal work with inheritance
- string copyFromExternal d3.js plot.js
+ string copyFromExternal .d3.js .plot.js
javascript
get marks() {
const x = this.get("x")
@@ -2620,8 +2620,8 @@ sparklineParser
extends abstractTableVisualizationParser
example
sparkline 1 2 3 4 5
- string copyFromExternal sparkline.js
- string requireOnce <script src="sparkline.js"></script>
+ string copyFromExternal .sparkline.js
+ string requireOnce <script src=".sparkline.js"></script>
catchAllAtomType numberAtom
// we need pattern matching
inScope scrollYParser
@@ -2754,10 +2754,10 @@ katexParser
katex
\text{E} = \text{T} / \text{A}!
description KaTex widget for typeset math.
- string copyFromExternal katex.min.css katex.min.js
+ string copyFromExternal .katex.min.css .katex.min.js
string requireOnce
- <link rel="stylesheet" href="katex.min.css">
- <script defer src="katex.min.js"></script>
+ <link rel="stylesheet" href=".katex.min.css">
+ <script defer src=".katex.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", () => document.querySelectorAll(".scrollKatex").forEach(el =>
{
@@ -2780,17 +2780,17 @@ helpfulNotFoundParser
popularity 0.000048
extends abstractScrollWithRequirementsParser
catchAllAtomType filePathAtom
- string copyFromExternal helpfulNotFound.js
+ string copyFromExternal .helpfulNotFound.js
description Helpful not found widget.
javascript
buildInstance() {
- return `<style>#helpfulNotFound{margin: 100px 0;}</style><h1 id="helpfulNotFound"></h1><script defer src="/helpfulNotFound.js"></script><script>document.addEventListener("DOMContentLoaded", () => new NotFoundApp('${this.content}'))</script>`
+ return `<style>#helpfulNotFound{margin: 100px 0;}</style><h1 id="helpfulNotFound"></h1><script defer src="/.helpfulNotFound.js"></script><script>document.addEventListener("DOMContentLoaded", () => new NotFoundApp('${this.content}'))</script>`
}
slideshowParser
// Left and right arrows navigate.
description Slideshow widget. *** delimits slides.
extends abstractScrollWithRequirementsParser
- string copyFromExternal jquery-3.7.1.min.js slideshow.js
+ string copyFromExternal .jquery-3.7.1.min.js .slideshow.js
example
slideshow
Why did the cow cross the road?
@@ -2801,19 +2801,19 @@ slideshowParser
****
javascript
buildHtml() {
- return `<style>html {font-size: var(--scrollBaseFontSize, 28px);} body {margin: auto; width: 500px;}.slideshowNav{text-align: center; margin-bottom:20px; font-size: 24px;color: rgba(204,204,204,.8);} a{text-decoration: none; color: rgba(204,204,204,.8);}</style><script defer src="jquery-3.7.1.min.js"></script><div class="slideshowNav"></div><script defer src="slideshow.js"></script>`
+ return `<style>html {font-size: var(--scrollBaseFontSize, 28px);} body {margin: auto; width: 500px;}.slideshowNav{text-align: center; margin-bottom:20px; font-size: 24px;color: rgba(204,204,204,.8);} a{text-decoration: none; color: rgba(204,204,204,.8);}</style><script defer src=".jquery-3.7.1.min.js"></script><div class="slideshowNav"></div><script defer src=".slideshow.js"></script>`
}
tableSearchParser
popularity 0.000072
extends abstractScrollWithRequirementsParser
- string copyFromExternal jquery-3.7.1.min.js datatables.css dayjs.min.js datatables.js tableSearch.js
+ string copyFromExternal .jquery-3.7.1.min.js .datatables.css .dayjs.min.js .datatables.js .tableSearch.js
string requireOnce
- <script defer src="jquery-3.7.1.min.js"></script>
+ <script defer src=".jquery-3.7.1.min.js"></script>
<style>.dt-search{font-family: "SF Pro", "Helvetica Neue", "Segoe UI", "Arial";}</style>
- <link rel="stylesheet" href="datatables.css">
- <script defer src="datatables.js"></script>
- <script defer src="dayjs.min.js"></script>
- <script defer src="tableSearch.js"></script>
+ <link rel="stylesheet" href=".datatables.css">
+ <script defer src=".datatables.js"></script>
+ <script defer src=".dayjs.min.js"></script>
+ <script defer src=".tableSearch.js"></script>
// adds to all tables on page
description Table search and sort widget.
javascript
@@ -3088,7 +3088,7 @@ aboveAsCodeParser
inspectBelowParser
description Inspect particle below.
extends belowAsCodeParser
- string copyFromExternal inspector.css
+ string copyFromExternal .inspector.css
javascript
get code() {
const mapFn = particle => {
@@ -3097,7 +3097,7 @@ inspectBelowParser
return this.selectedParticles.map(mapFn).join("<br>")
}
buildHtml() {
- return `<link rel="stylesheet" href="inspector.css">` + this.code
+ return `<link rel="stylesheet" href=".inspector.css">` + this.code
}
inspectAboveParser
description Inspect particle above.
@@ -3297,7 +3297,7 @@ qrcodeParser
if (isNode) {
const {externalsPath} = this.root
const path = require("path")
- const {qrcodegen, toSvgString} = require(path.join(externalsPath, "qrcodegen.js"))
+ const {qrcodegen, toSvgString} = require(path.join(externalsPath, ".qrcodegen.js"))
const QRC = qrcodegen.QrCode;
const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);
const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo
@@ -3958,14 +3958,14 @@ scrollThemeParser
extends abstractScrollParser
catchAllAtomType scrollThemeAtom
description A collection of simple themes.
- string copyFromExternal gazette.css
+ string copyFromExternal .gazette.css
// Note this will be replaced at runtime
javascript
get copyFromExternal() {
return this.files.join(" ")
}
get files() {
- return this.atoms.slice(1).map(name => `${name}.css`)
+ return this.atoms.slice(1).map(name => `.${name}.css`)
}
buildHtml() {
return this.files.map(name => `<link rel="stylesheet" type="text/css" href="${name}">`).join("\n")
@@ -5647,7 +5647,7 @@ scrollParser
}
get scrollVersion() {
// currently manually updated
- return "161.3.0"
+ return "162.0.0"
}
// Use the first paragraph for the description
// todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)
------------------------------------------------------------
commit f6682254e0a3315e7377dd1ec195b691483d3d6c
Author: Breck Yunits <breck7@gmail.com>
Date: Wed Dec 4 21:25:25 2024 -1000
diff --git a/dist/constants.js b/dist/constants.js
index d7ef43a..02b0dd1 100644
--- a/dist/constants.js
+++ b/dist/constants.js
@@ -1 +1 @@
-const AppConstants = {"parsers":"columnNameAtom\n extends stringAtom\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n extends stringAtom\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint comment\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n extends anyAtom\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\nscriptAnyAtom\n extends codeAtom\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom keywordAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint string\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\njavascriptAtom\n extends stringAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\"<a \") ? false : true\n return shouldLinkify ? this.replaceNotes(Utils.linkify(originalText)) : originalText\n }\n replaceNotes(originalText) {\n // Skip the replacements if there are no footnotes or the text has none.\n if (!this.root.footnotes.length || !originalText.includes(\"^\")) return originalText\n this.root.footnotes.forEach((note, index) => {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `<a href=\"#${note.htmlId}\" class=\"scrollNoteLink\" id=\"${linkBack}\"><sup>${note.label}</sup></a>`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.text}${this.closingTag}`\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return `</${tag}>`\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `<span class=\"scrollBlink\">${super.buildHtml()}</span>\n <script>setInterval(()=>{ Array.from(document.getElementsByClassName(\"scrollBlink\")).forEach(el => el.style.visibility = el.style.visibility === \"hidden\" ? \"visible\" : \"hidden\") }, 500)</script>`\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"linkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"</center>\")\n return `<center>${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser\n javascript\n compileSubparticles() {\n return this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildHtml() {\n return super.buildHtml() + this.compileSubparticles()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `<div style=\"text-indent:${(this.getIndentLevel() - 1) * 20}px;\"><input type=\"checkbox\" ${this.checked} id=\"${this.id}\"><label for=\"${this.id}\">` + super.text + `</label></div>`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `</${listType}>` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return `<style>\\n.${this.constructor.name} li::marker {content: \"${this.cue} \";}\\n</style>`\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* <span id=\"counter${id}\" title=\"0\">0</span><script>setInterval(()=>{ const el = document.getElementById('counter${id}'); el.title = parseFloat(el.title) + ${increment}; el.textContent = Math.floor(parseFloat(el.title)).toLocaleString()}, 100)</script> ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"</details>\")\n return `<details>${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `<a class=\"scrollFootNoteUsageLink\" href=\"#noteUsage${this.noteDefinitionIndex}\">${this.label}</a> ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"</div>\")\n return `<div class=\"scrollSection\">` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}></${this.tag}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return `<audio controls ${this.getAsHtmlAttributes(\"loop autoplay\".split(\" \"))}><source src=\"${this.filename}\" type=\"audio/mpeg\"></audio>`\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom\n heightParser\n cueFromId\n atoms cueAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\n widthParser\n // todo: fix inheritance bug\n cueFromId\n atoms cueAtom\n heightParser\n cueFromId\n atoms cueAtom\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* <span class=\"scrollStopwatchParser\" id=\"stopwatch${id}\">0.0</span><script>{let startTime = parseFloat(new URLSearchParams(window.location.search).get('start') || 0); document.getElementById('stopwatch${id}').title = startTime; setInterval(()=>{ const el = document.getElementById('stopwatch${id}'); el.title = parseFloat(el.title) + .1; el.textContent = (parseFloat(el.title)).toFixed(1)}, 100)}</script> `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"</div>\") // Single columns are self-closing after section break.\n return stackContents + `<div class=\"scrollColumns\" style=\"column-width:${columnWidth}ch;column-count:${maxColumns};max-width:${maxTotalWidth}ch;\">`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"</div>\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"</div>\")\n return `<style>.scrollContainerParser{width: 100%; box-sizing: border-box; max-width: ${this.maxWidth}; margin: 0 auto;}</style>` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `<div class=\"${this.defaultClass}\"><span>${this.dinkus}</span></div>`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n javascript\n buildHtml() {\n return `<hr>`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `<style>.abstractIconButtonParser {position:absolute;top:0.25rem; }.abstractIconButtonParser svg {fill: rgba(204,204,204,.8);width:1.875rem;height:1.875rem; padding: 0 7px;} .abstractIconButtonParser:hover svg{fill: #333;}</style><a href=\"${this.link}\" class=\"doNotPrint abstractIconButtonParser\" style=\"${this.style}\">${this.svg}</a>`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg <svg fill=\"#000000\" xmlns=\"http://www.w3.org/2000/svg\" width=\"800px\" height=\"800px\" viewBox=\"0 0 52 52\" enable-background=\"new 0 0 52 52\" xml:space=\"preserve\"><path d=\"M38.6,20.4c-1-6.5-6.7-11.5-13.5-11.5c-7.6,0-13.7,6.1-13.7,13.7c0,0.3,0,0.7,0.1,1c-5,0.4-8.9,4.6-8.9,9.6 c0,5.4,4.3,9.7,9.7,9.7h11.5c-0.8-0.8-8.1-8.1-8.1-8.1c-0.4-0.4-0.4-0.9,0-1.3l1.3-1.3c0.4-0.4,0.9-0.4,1.3,0l3.5,3.5 c0.4,0.4,1.1,0.1,1.1-0.4V21.8c0-0.4,0.5-0.9,1-0.9h1.9c0.5,0,0.9,0.4,0.9,0.9v13.4c0,0.6,0.8,0.8,1.1,0.4l3.5-3.5 c0.4-0.4,0.9-0.4,1.3,0l1.3,1.3c0.4,0.4,0.4,0.9,0,1.3L26,42.9h12.3v0c6.1-0.1,11-5.1,11-11.3C49.4,25.5,44.6,20.6,38.6,20.4z\"/></svg><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->\n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg <svg width=\"800px\" height=\"800px\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M21.1213 2.70705C19.9497 1.53548 18.0503 1.53547 16.8787 2.70705L15.1989 4.38685L7.29289 12.2928C7.16473 12.421 7.07382 12.5816 7.02986 12.7574L6.02986 16.7574C5.94466 17.0982 6.04451 17.4587 6.29289 17.707C6.54127 17.9554 6.90176 18.0553 7.24254 17.9701L11.2425 16.9701C11.4184 16.9261 11.5789 16.8352 11.7071 16.707L19.5556 8.85857L21.2929 7.12126C22.4645 5.94969 22.4645 4.05019 21.2929 2.87862L21.1213 2.70705ZM18.2929 4.12126C18.6834 3.73074 19.3166 3.73074 19.7071 4.12126L19.8787 4.29283C20.2692 4.68336 20.2692 5.31653 19.8787 5.70705L18.8622 6.72357L17.3068 5.10738L18.2929 4.12126ZM15.8923 6.52185L17.4477 8.13804L10.4888 15.097L8.37437 15.6256L8.90296 13.5112L15.8923 6.52185ZM4 7.99994C4 7.44766 4.44772 6.99994 5 6.99994H10C10.5523 6.99994 11 6.55223 11 5.99994C11 5.44766 10.5523 4.99994 10 4.99994H5C3.34315 4.99994 2 6.34309 2 7.99994V18.9999C2 20.6568 3.34315 21.9999 5 21.9999H16C17.6569 21.9999 19 20.6568 19 18.9999V13.9999C19 13.4477 18.5523 12.9999 18 12.9999C17.4477 12.9999 17 13.4477 17 13.9999V18.9999C17 19.5522 16.5523 19.9999 16 19.9999H5C4.44772 19.9999 4 19.5522 4 18.9999V7.99994Z\"/></svg>\n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg <svg viewBox=\"3 5 24 20\" width=\"24\" height=\"20\" xmlns=\"http://www.w3.org/2000/svg\"><g transform=\"matrix(1, 0, 0, 1, 0, -289.0625)\"><path style=\"opacity:1;stroke:none;stroke-width:0.49999997;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1\" d=\"M 5 5 C 4.2955948 5 3.6803238 5.3628126 3.3242188 5.9101562 L 14.292969 16.878906 C 14.696939 17.282876 15.303061 17.282876 15.707031 16.878906 L 26.675781 5.9101562 C 26.319676 5.3628126 25.704405 5 25 5 L 5 5 z M 3 8.4140625 L 3 23 C 3 24.108 3.892 25 5 25 L 25 25 C 26.108 25 27 24.108 27 23 L 27 8.4140625 L 17.121094 18.292969 C 15.958108 19.455959 14.041892 19.455959 12.878906 18.292969 L 3 8.4140625 z \" transform=\"translate(0,289.0625)\" id=\"rect4592\"/></g></svg>\n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg <svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12.7166 3.79541C12.2835 3.49716 11.7165 3.49716 11.2834 3.79541L4.14336 8.7121C3.81027 8.94146 3.60747 9.31108 3.59247 9.70797C3.54064 11.0799 3.4857 13.4824 3.63658 15.1877C3.7504 16.4742 4.05336 18.1747 4.29944 19.4256C4.41371 20.0066 4.91937 20.4284 5.52037 20.4284H8.84433C8.98594 20.4284 9.10074 20.3111 9.10074 20.1665V15.9754C9.10074 14.9627 9.90433 14.1417 10.8956 14.1417H13.4091C14.4004 14.1417 15.204 14.9627 15.204 15.9754V20.1665C15.204 20.3111 15.3188 20.4284 15.4604 20.4284H18.4796C19.0806 20.4284 19.5863 20.0066 19.7006 19.4256C19.9466 18.1747 20.2496 16.4742 20.3634 15.1877C20.5143 13.4824 20.4594 11.0799 20.4075 9.70797C20.3925 9.31108 20.1897 8.94146 19.8566 8.7121L12.7166 3.79541ZM10.4235 2.49217C11.3764 1.83602 12.6236 1.83602 13.5765 2.49217L20.7165 7.40886C21.4457 7.91098 21.9104 8.73651 21.9448 9.64736C21.9966 11.0178 22.0564 13.5119 21.8956 15.3292C21.7738 16.7067 21.4561 18.4786 21.2089 19.7353C20.9461 21.0711 19.7924 22.0001 18.4796 22.0001H15.4604C14.4691 22.0001 13.6655 21.1791 13.6655 20.1665V15.9754C13.6655 15.8307 13.5507 15.7134 13.4091 15.7134H10.8956C10.754 15.7134 10.6392 15.8307 10.6392 15.9754V20.1665C10.6392 21.1791 9.83561 22.0001 8.84433 22.0001H5.52037C4.20761 22.0001 3.05389 21.0711 2.79113 19.7353C2.54392 18.4786 2.22624 16.7067 2.10437 15.3292C1.94358 13.5119 2.00338 11.0178 2.05515 9.64736C2.08957 8.73652 2.55427 7.91098 3.28346 7.40886L10.4235 2.49217Z\"/></svg>\n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg <svg xmlns=\"http://www.w3.org/2000/svg\" direction=\"ltr\" width=\"231.52568231268793\" height=\"249.33156169975996\" viewBox=\"297.27169239534896 1273.121785992385 231.52568231268793 249.33156169975996\" stroke-linecap=\"round\" stroke-linejoin=\"round\" data-color-mode=\"dark\" class=\"tl-container tl-theme__force-sRGB tl-theme__dark\" ><defs/><g transform=\"matrix(1, 0, 0, 1, 395.9682, 1413.3618)\" opacity=\"1\"><g transform=\"scale(1)\"><path d=\"M-5.7342,-1.1484 T-5.0182,-4.6536 -2.6672,-12.9768 1.7374,-22.3903 8.4597,-30.0892 16.5455,-35.2796 25.3478,-37.8247 34.4641,-37.2199 43.0676,-33.4606 50.27,-27.1118 55.7861,-19.7972 59.8042,-11.6764 61.6844,-2.3157 60.8049,7.9615 57.3259,17.5954 51.2605,25.5515 41.9451,31.9237 30.6489,36.2084 18.9812,37.6038 8.4915,36.637 -1.3063,34.5174 -10.6869,31.6604 -19.0463,27.0252 -26.0612,20.1562 -31.864,11.1271 -35.2772,1.0066 -35.7963,-9.1933 -35.203,-19.2337 -32.52,-28.3259 -27.3388,-37.4245 -20.2857,-45.7085 -11.787,-53.0395 -2.4314,-58.4864 7.1724,-61.8739 17.5002,-65.6025 28.2859,-68.1034 38.514,-69.1081 48.1844,-69.9134 57.9136,-70.1787 67.4056,-69.5971 76.297,-67.7446 84.5233,-63.97 92.0158,-57.5003 98.1976,-48.5614 102.8839,-38.056 105.9936,-27.6097 107.074,-18.168 107.175,-8.2882 106.6452,1.9768 105.4734,11.7698 104.0279,21.1013 101.7439,31.0445 98.0977,40.6386 92.7822,49.1039 86.2847,57.2337 78.6099,64.7113 69.8339,70.974 61.2863,75.092 52.4283,78.2186 42.5338,80.7945 32.7733,82.6629 22.8568,83.6574 12.2803,83.3538 2.4971,81.4828 -6.3367,78.789 -15.1061,75.8011 -24.259,72.3779 -33.2286,68.991 -41.5707,64.8363 -49.153,59.1909 -55.8375,52.2938 -62.9311,43.9553 -68.4604,34.4882 -71.2761,23.5855 -72.748,12.7452 -73.7673,2.0453 -74.1187,-8.7386 -72.293,-19.3856 -68.7376,-30.4053 -64.12,-39.7852 -59.1825,-47.8931 -53.7695,-56.4512 -48.1998,-65.7676 -43.1389,-74.401 -36.9006,-81.0179 -28.4317,-86.2639 -18.3342,-90.3521 -8.9259,-93.9355 0.1927,-98.7179 9.3551,-103.3537 18.7651,-106.7552 29.7482,-110.3584 40.9287,-113.446 50.87,-115.2231 57.8279,-116.0074 60.4065,-116.0632 A7.8326,7.8326 0 0 1 61.1735,-100.4168 T58.6018,-100.2201 51.1634,-99.3008 41.695,-96.884 32.4044,-94.0187 22.65,-91.382 13.4974,-87.3756 5.0519,-83.1026 -5.0443,-78.6175 -15.7236,-74.4743 -24.6193,-70.2681 -31.274,-62.5903 -36.3827,-53.9362 -41.2833,-46.2905 -46.0679,-38.5161 -50.639,-30.6576 -54.6876,-22.4063 -57.6038,-12.3346 -58.5104,-1.8678 -57.6022,9.4969 -56.0628,20.477 -53.6434,29.0487 -48.8908,36.7517 -42.4075,43.9784 -35.3064,50.6236 -27.3553,55.2062 -17.6884,59.4064 -7.6442,63.6638 2.4427,67.0449 12.7308,69.4843 22.9294,70.2171 33.2547,69.3921 42.4972,67.7384 52.2753,64.8425 61.8649,60.9142 69.5292,56.0828 76.3604,49.5927 82.431,42.3194 87.219,34.619 90.6076,25.5217 92.547,16.0352 93.8703,7.1315 94.4972,-2.5268 94.3672,-13.3681 93.3204,-24.4252 90.5377,-34.1656 85.7321,-43.6441 78.7334,-51.8057 68.7542,-56.0559 57.9857,-57.2713 48.079,-57.0198 38.4535,-56.3204 28.5246,-55.0036 18.6104,-52.4796 9.6402,-49.559 0.7193,-46.2981 -7.1531,-41.3715 -13.708,-34.8957 -19.5435,-26.853 -22.8356,-17.0622 -23.363,-6.5446 -21.9969,3.1439 -17.1086,11.9113 -9.276,18.744 -0.0685,22.6058 9.3636,24.796 19.2422,25.8817 29.052,25.0411 37.9338,21.3502 45.6819,13.8498 49.6675,4.5228 49.2802,-5.1854 45.3277,-14.4455 38.8603,-21.7879 30.5488,-25.7753 21.3356,-24.3975 13.6687,-19.2391 8.8217,-10.9919 6.423,-2.3609 5.7342,1.1484 A5.8481,5.8481 0 0 1 -5.7342,-1.1484 Z\" stroke-linecap=\"round\"/></g></g></svg>\n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return `<div class=\"abstractTextLinkParser\"><a href=\"${this.link}\">${this.text}</a></div>`\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n <script>\n sendFormViaEmail = form => {\n const mailto = new URL(\"mailto:\")\n const params = []\n const { value, title } = form.querySelector('button[type=\"submit\"]')\n params.push(`subject=${encodeURIComponent(value)}`)\n params.push(`to=${encodeURIComponent(title)}`)\n const oneTextarea = form.querySelector('textarea[title=\"oneTextarea\"]')\n const body = oneTextarea ? codeMirrorInstance.getValue() : Array.from(new FormData(form)).map(([name, value]) => `${name} ${value}`).join(\"\\\\n\")\n params.push(`body=${encodeURIComponent(body)}`)\n mailto.search = params.join(\"&\")\n window.open(mailto.href, '_blank')\n }\n </script>\n string style\n <style> .scrollFormParser {\n font-family: \"Gill Sans\", \"Bitstream Vera Sans\", sans-serif;\n }\n .scrollFormParser input , .scrollFormParser textarea{\n padding: 10px;\n margin-bottom: 10px;\n width: 100%;\n box-sizing: border-box;\n } .scrollFormParser label {\n display: block;\n margin-bottom: 5px;\n }\n </style>\n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = `<textarea placeholder=\"${placeholder}\" id=\"${Name}\" name=\"${Name}\" ${IsRequired ? \"required\" : \"\"}></textarea>`\n else\n tag = `<input placeholder=\"${placeholder}\" type=\"${type}\" id=\"${Name}\" name=\"${Name}\" ${IsRequired ? \"required\" : \"\"}>`\n return `<div><label for=\"${Name}\" title=\"${IsRequired ? \"Required\" : \"\"}\">${ucFirst}${IsRequired ? \"*\" : \"\"}:</label>${tag}</div>`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}<form ${isEmail ? \"onsubmit='sendFormViaEmail(this); return false;'\" : ` method='post' action='${formDestination}'`} class=\"scrollFormParser\">${this.inputs}<button value=\"${subject}\" title=\"${formDestination}\" class=\"scrollButton\" type=\"submit\">${callToAction}</button>${this.footer}</form>`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal codeMirror.css scrollLibs.js constants.js\n string requireOnce\n <link rel=\"stylesheet\" href=\"codeMirror.css\">\n <script src=\"scrollLibs.js\"></script>\n <script src=\"constants.js\"></script>\n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `<textarea title=\"oneTextarea\" rows=\"${Math.min(this.root.measures.length * 2, 30)}\" placeholder=\"${this.placeholder}\" id=\"${Name}\" name=\"${Name}\"></textarea>\n <script id=\"${Name}Parsers\" type=\"text/plain\">${this.parsersBundle}</script>\n <script>{\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n let {width, height} = document.getElementById('${Name}').getBoundingClientRect();\n const sp = new Particle(AppConstants.parsers)\n sp.filter(particle => particle.getLine().match(parserRegex)).forEach(part => {\n part.appendLine(\"boolean suggestInAutocomplete false\")\n })\n const customScrollParser = new HandParsersProgram(sp.toString() + document.getElementById(\"${Name}Parsers\").textContent).compileAndReturnRootParser()\n codeMirrorInstance = new ParsersCodeMirrorMode(\"custom\", () => customScrollParser, undefined, CodeMirror).register().fromTextAreaWithAutocomplete(document.getElementById(\"${Name}\"), {\n lineWrapping: false,\n lineNumbers: false\n })\n codeMirrorInstance.setSize(width, height);\n codeMirrorInstance.setValue(\\`${this.value}\\`); }</script>`\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `<a class=\"scrollContinueReadingLink\" href=\"${linkRelativeToCompileTarget}\">Continue reading...</a>`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `<div class=\"scrollSnippetContainer\">${this.makeSnippet(file.file.scrollProgram, buildSettings)}</div>`\n }).join(\"\\n\\n\")\n return `<div class=\"scrollColumns\" style=\"column-width:35ch;\">${snippets}</div>`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue nav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return `<nav class=\"scrollNavParser\">` + this.root.getFilesByTags(this.content).map(file => {\n const { linkTitle, permalink } = file.file.scrollProgram\n return `<a href=\"${permalink}\">${linkTitle}</a>` \n }).join(this.get(\"join\") || \" \") + `</nav>`\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `<div><a href=\"${permalink}\">${title}</a><div>${description}...</div><div class=\"subdued\" style=\"text-align:right;\">${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}</div></div>`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `<div id=\"${id}\" class=\"scrollNoticesParser\" style=\"display:none;\"></div><script>(function(){\n const params = new URLSearchParams(window.location.search)\n if (!params.size) return\n document.getElementById('${id}').innerHTML = Array.from(params.entries()).map(([key, value]) => {\n if (key === \"error\") \n return '<div style=\"color:red\">Error: ' + value + '</div>'\n if (key === \"success\")\n return '<div style=\"color:green\">Success: ' + value + '</div>'\n return '<div>' + key + ': ' + value + '</div>'\n }).join(\"\") || \"No query parameters found\"\n document.getElementById('${id}').style.display = \"block\"\n })()</script>`\n }\nprintSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.buildTxt().replace(/\\</g, \"<\")}</code>`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual} = this\n const expected = this.subparticlesToString()\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType anyAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n javascript\n buildHtml() {\n return `<div class=\"printDateParser\">${this.day}</div>`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne() { await this._buildFileType(this.extension) }\nbuildCsvParser\n popularity 0.000096\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo() {\n await this._buildFileType(this.extension)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(externalFilesCopied) {\n await this._copyExternalFiles(externalFilesCopied)\n await super.buildTwo()\n }\n async _copyExternalFiles(externalFilesCopied = {}) {\n if (!this.isNodeJs()) return\n const {root} = this\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Write RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Write concepts to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom anyAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildCommandParser\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Write measures to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom anyAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo() {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${this.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the <html lang=\"\"> tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Meta tag description.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType anyAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType anyAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `<div style=\"text-align: ${index % 2 ? \"right\" : \"left\"};\" class=\"scrollChat ${index % 2 ? \"scrollChatRight\" : \"scrollChatLeft\"}\"><span>${line.asString}</span></div>` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = typeof d3 === \"undefined\" ? require('d3') : d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = typeof d3 === \"undefined\" ? require('d3') : d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/</g, \"<\")\n return {\n title, titleLink: relativePath + permalink, text, date, wordCount, minutes\n }\n }\n columnNames = \"title titleLink text date wordCount minutes\".split(\" \")\nscrollPostsParser\n popularity 0.000024\n cue posts\n description Posts as table.\n extends abstractPostsParser\n example\n // Print a search table:\n posts\n printTable\n tableSearch\n // Dump a CSV of blog:\n buildHtml\n buildCsv\n buildJson\nscrollPostsMetaParser\n popularity 0.000024\n cue postsMeta\n description Post meta as table.\n extends abstractPostsParser\n javascript\n columnNames = [\"date\", \"year\", \"title\", \"permalink\", \"authors\", \"tags\", \"wordCount\", \"minutes\"]\n postToRow(file) {\n const {date, year, title, permalink, authors, tags, wordCount, minutes} = file.file.scrollProgram\n return {\n date, year, title, permalink, authors, tags, wordCount, minutes\n }\n }\nprintFeedParser\n popularity 0.000048\n description Print group to RSS.\n extends abstractPostsParser\n example\n printFeed index\n printFeed cars/index\n buildRss\n javascript\n buildRss() {\n const {dayjs} = this.root\n const scrollPrograms = this.files.map(file => file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>\n <rss version=\"2.0\">\n <channel>\n <title>${title}</title>\n <link>${baseUrl}</link>\n <description>${description}</description>\n <lastBuildDate>${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}</lastBuildDate>\n <language>en-us</language>\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n </channel>\n </rss>`\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.code.replace(/\\</g, \"<\")}</code>`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `<div class=\"codeWithHeader\"><div class=\"codeHeader\">${this.content}</div>${super.buildHtml()}</div>`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n <script>\n document.addEventListener(\"DOMContentLoaded\", () => document.querySelectorAll(\".scrollCodeBlock\").forEach(block =>\n {\n if (!navigator.clipboard) return\n const button = document.createElement(\"span\")\n button.classList.add(\"scrollCopyButton\")\n block.appendChild(button)\n button.addEventListener(\"click\", async () => {\n await navigator.clipboard.writeText(block.innerText)\n button.classList.add(\"scrollCopiedButton\")\n })\n }\n ))\n </script>\nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = `<style>\n .heatrixContainer {\n margin: auto;\n }.heatrixRow {white-space: nowrap;}\n ${cssId} .heatrixAtom {\n font-family: arial;\n border-radius: 2px;\n border: 1px solid transparent;\n display: inline-block;\n margin: 1px;\n text-align: center;\n vertical-align: middle;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n .heatrixAtom a {\n color: black;\n }\n ${cssId} .heatrixAtom{\n width: ${defaultWidth};\n height: ${defaultHeight};\n font-size: ${fontSize};\n line-height: ${lineHeight};\n }\n </style>`\n const firstRow = this.table[0]\n return (\n `<div class=\"heatrixContainer\" id=\"heatrix${this.uid}\">${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `<div class=\"heatrixRow heatrixRow${rowIndex}\">${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return `<div class=\"heatrixAtom heatrixColumn${columnIndex}${valueClass}\" style=\"${inlineStyle}\"><a title=\"${label}\" ${href}>${label}</a></div>`\n })\n .join(\"\")}</div>`\n })\n .join(\"\\n\") +\n \"</div>\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom anyAtom\n cueFromId\n single\n colorParser\n atoms cueAtom anyAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType anyAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal leaflet.css leaflet.js scrollLibs.js\n string requireOnce\n <link rel=\"stylesheet\" href=\"leaflet.css\">\n <script src=\"leaflet.js\"></script>\n <script src=\"scrollLibs.js\"></script>\n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `<div id=\"${mapId}\" style=\"${style}\"></div>\n <script>\n {\n if (!window.maps) window.maps = {}\n const moveToMyLocation = () => {\n if (!navigator.geolocation) return\n navigator.geolocation.getCurrentPosition((position) => {\n const { latitude, longitude } = position.coords\n window.maps.${mapId}.setView([latitude, longitude])\n L.circleMarker([latitude, longitude], {\n fillOpacity: 0.8,\n radius: 20,\n weight: 4\n })\n .addTo(window.maps.${mapId})\n }, () => {})\n }\n const lat = ${this.get(\"lat\") ?? 37.8}\n const long = ${this.get(\"long\") ?? 4}\n if (${this.has(\"geolocate\")}){\n moveToMyLocation()\n }\n const zoomLevel = ${this.get(\"zoom\") ?? 4}\n const hover = '${this.get(\"hover\") || \"<b>{title}</b><br>{description}\"}'\n const template = ${JSON.stringify(template)}\n const points = ${JSON.stringify((this.parent.coreTable || []).filter(point => point.lat && point.long), undefined, 2)}\n window.maps.${mapId} = L.map(\"map${id}\").setView([lat, long], zoomLevel)\n const map = window.maps.${mapId}\n const tileOptions = {\n \"default\": {\n baseLayer: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',\n attribution: '<a href=\"https://www.openstreetmap.org/\">OpenStreetMap</a> contributors'\n },\n light: {\n baseLayer: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',\n attribution: '<a href=\"https://www.openstreetmap.org/\">OpenStreetMap</a> contributors <a href=\"https://carto.com/ attributions\">CARTO</a>'\n },\n }\n const {baseLayer, attribution} = tileOptions.${this.get(\"tiles\") || \"default\"}\n L.tileLayer(baseLayer, {\n attribution,\n maxZoom: 19\n }).addTo(map);\n points.forEach(point => {\n L.circleMarker([point.lat, point.long], {...template, ...Object.fromEntries(\n Object.entries(point).filter(([key, value]) => value !== null)\n )})\n .addTo(map)\n .bindPopup(new Particle(point).evalTemplateString(hover))\n })\n }\n </script>`\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal d3.js plot.js\n string requireOnce\n <script src=\"d3.js\"></script>\n <script src=\"plot.js\"></script>\n example\n plot\n inScope abstractColumnNameParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `<div id=\"${id}\"></div><script>\n {\n let loadChart = async () => {\n const data = ${this.dataCode}\n const get = (col, index ) => col !== \"undefined\" ? col : (index === undefined ? undefined : Object.keys(data[0])[index])\n document.querySelector(\"#${id}\").append(Plot.plot(${this.plotOptions}))\n }\n loadChart()\n }\n </script>`\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n }`\n }\nscatterplotParser\n extends abstractPlotParser\n description Scatterplot Widget.\n // todo: make copyFromExternal work with inheritance\n string copyFromExternal d3.js plot.js\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true,\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal sparkline.js\n string requireOnce <script src=\"sparkline.js\"></script>\n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return `<span id=\"${id}\"></span><script>new Sparkline(document.getElementById(\"${id}\"), {dotRadius: 0, width: ${width}, height: ${height}, lineColor: \"${lineColor}\", tooltip: (value,index) => ${start} + index + \": \" + value}).draw(${JSON.stringify(columnValues)})</script>`\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `<th>${column.name}</th>\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `<a href=\"${link}\">${text}</a>`\n else if (col.name.endsWith(\"Url\")) tagged = `<a href=\"${content}\">${col.name.replace(\"Url\", \"\")}</a>`\n else if (isUrl) tagged = `<a href=\"${content}\">${text}</a>`\n str += `<td>${tagged}</td>\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `<tr>${this.toRow(row)}</tr>`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `<table id=\"table${this._getUid()}\" class=\"scrollTable\">\n <thead><tr>${this.tableHeader.join(\"\\n\")}</tr></thead>\n <tbody>${this.tableBody}</tbody>\n </table>`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal katex.min.css katex.min.js\n string requireOnce\n <link rel=\"stylesheet\" href=\"katex.min.css\">\n <script defer src=\"katex.min.js\"></script>\n <script>\n document.addEventListener(\"DOMContentLoaded\", () => document.querySelectorAll(\".scrollKatex\").forEach(el =>\n {\n katex.render(el.innerText, el, {\n throwOnError: false\n });\n }\n ))\n </script>\n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `<div class=\"scrollKatex\" id=\"${id}\">${content + this.subparticlesToString()}</div>`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `<style>#helpfulNotFound{margin: 100px 0;}</style><h1 id=\"helpfulNotFound\"></h1><script defer src=\"/helpfulNotFound.js\"></script><script>document.addEventListener(\"DOMContentLoaded\", () => new NotFoundApp('${this.content}'))</script>`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal jquery-3.7.1.min.js slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `<style>html {font-size: var(--scrollBaseFontSize, 28px);} body {margin: auto; width: 500px;}.slideshowNav{text-align: center; margin-bottom:20px; font-size: 24px;color: rgba(204,204,204,.8);} a{text-decoration: none; color: rgba(204,204,204,.8);}</style><script defer src=\"jquery-3.7.1.min.js\"></script><div class=\"slideshowNav\"></div><script defer src=\"slideshow.js\"></script>`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal jquery-3.7.1.min.js datatables.css dayjs.min.js datatables.js tableSearch.js\n string requireOnce\n <script defer src=\"jquery-3.7.1.min.js\"></script>\n <style>.dt-search{font-family: \"SF Pro\", \"Helvetica Neue\", \"Segoe UI\", \"Arial\";}</style>\n <link rel=\"stylesheet\" href=\"datatables.css\">\n <script defer src=\"datatables.js\"></script>\n <script defer src=\"dayjs.min.js\"></script>\n <script defer src=\"tableSearch.js\"></script>\n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>${this.css}</style>`\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCs() {\n return this.css\n }\ninlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractScrollParser\n cueFromId\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return `<style>/* ${this.content} */\\n${this.css}</style>`\n }\n get css() {\n return this.atoms.slice(1).map(filename => this.root.readFile(filename)).join(\"\\n\\n\")\n }\n buildCs() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>html, body { background: ${this.content};}</style>`\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>html, body { color: ${this.content};}</style>`\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return `<style>html, body, h1,h2,h3 { font-family: ${font};}</style>`\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return `<link rel=\"stylesheet\" type=\"text/css\" href=\"${this.filename}\">`\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return `<script src=\"${this.filename}\"></script>`\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `<tr>` + items.map(particle => `<td>${particle.cue}<span>${particle.content}</span></td>`).join(\"\\n\") + `</tr>\\n`\n }\n buildHtml() {\n return `<table class=\"scrollDashboard\">${this.tableBody}</table>`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = this[method]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = next[method]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.code.replace(/\\</g, \"<\")}</code>`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType anyAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\ninspectBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal inspector.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `<div class=\"inspectorParticle\"><span class=\"inspectorParticleId\">${particle.constructor.name}</span>${particle.atoms.map((atom, index) => `<span class=\"inspectorAtom\">${atom}<span class=\"inspectorAtomType\">${atomTypes[index]}</span></span>`).join(\" \")}${(particle.length ? `<br><div class=\"inspectorSubparticles\">` + particle.map(mapFn).join(\"<br>\") + `</div>` : \"\")}</div>`}\n return this.selectedParticles.map(mapFn).join(\"<br>\")\n }\n buildHtml() {\n return `<link rel=\"stylesheet\" href=\"inspector.css\">` + this.code\n }\ninspectAboveParser\n description Inspect particle above.\n extends inspectBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return `<style>${this.css}</style>`\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCs() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(`</${tag}>`)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `<br>`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => `<iframe src=\"${url}\" frameborder=\"0\"></iframe>`).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `<figcaption>${caption.buildHtml()}</figcaption>` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `<figure class=\"scrollCaptionedFigure\" style=\"${widthStyle + float}\">${this.getFigureContent(buildSettings)}${captionFig}</figure>`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser linkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"linkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return `<a href=\"${clickLink}\" target=\"${target}\" ${className} ${id}><img src=\"${linkRelativeToCompileTarget}\" ${dimensionAttributes}loading=\"lazy\"></a>`\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \"qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `<div class=\"scrollYouTubeHolder\"><iframe class=\"scrollYouTubeEmbed\" src=\"${url}\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe></div>`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\ninlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractScrollParser\n cueFromId\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return `<script>/* ${this.content} */\\n${this.contents}</script>`\n }\n get contents() {\n return this.atoms.slice(1).map(filename => this.root.readFile(filename)).join(\";\\n\\n\")\n }\n buildJs() {\n return this.contents\n }\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType scriptAnyAtom\n javascript\n buildHtml() {\n return `<script>${this.scriptContent}</script>`\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return `<script>window.${varName} = ${this.root.readFile(this.filename)}</script>`\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<style>${style}</style><a class=\"keyboardNav doNotPrint\" style=\"left:.5rem;\" href=\"${linkToPrevious}\"><</a><a class=\"keyboardNav doNotPrint\" style=\"right:.5rem;\" href=\"${linkToNext}\">></a>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = `<script>document.addEventListener('keydown', function(event) {\n if (document.activeElement !== document.body) return\n if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return // dont interfere with keyboard back button shortcut\n const getLinks = () => document.getElementsByClassName(\"scrollKeyboardNav\")[0].getElementsByTagName(\"a\")\n if (event.key === \"ArrowLeft\")\n getLinks()[0].click()\n else if (event.key === \"ArrowRight\")\n getLinks()[1].click()\n });</script>`\n return `<div class=\"scrollKeyboardNav\" style=\"display:none;\"><a href=\"${linkToPrevious}\">${linkToPrevious}</a> · ${root.permalink} · <a href=\"${linkToNext}\">${linkToNext}</a>${script}</div>`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"<h></h>\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `<style>${css}</style><div id=\"scrollLeetSheet\">` + doc.map(obj => `<div class=\"${obj.category}\"><a href=\"${this.makeLink(obj.examples, obj.id)}\">${obj.isPopular ? \"<b>\" : \"\"}${obj.id}</a> ${obj.description}${obj.isPopular ? \"</b>\" : \"\"}${obj.note}</div>`).join(\"\\n\") + \"</div>\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n buildCsv() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category} = obj\n return {\n id,\n isPopular,\n description,\n popularity,\n category\n }\n })\n return new Particle(this.root.lodash.sortBy(rows, \"isPopular\")).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"<p><b>Parser Definition Parsers</b> define parsers that acquire, analyze and act on code.</p>\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"<p><b>Atom Definition Parsers</b> analyze the atoms in a line.</p>\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` <span class=\"note\">A${category.replace(\"Phase\", \"\").substr(1)}Time.</span>`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `<link rel=\"icon\" href=\"${favicon}\">` : \"\"\n const rssTag = rssFeedUrl ? `<link rel=\"alternate\" type=\"application/rss+xml\" title=\"${title}\" href=\"${rssFeedUrl}\">` : \"\"\n const gitTag = gitRepo ? `<link rel=\"source\" type=\"application/git\" title=\"Source Code Repository\" href=\"${gitRepo}\">` : \"\"\n return `<head>\n <meta charset=\"utf-8\">\n <title>${title}</title>\n <script>/* This HTML was generated by 📜 Scroll v${scrollVersion}. https://scroll.pub */</script>\n <style>@media print {.doNotPrint {display: none !important;}}</style>\n <link rel=\"canonical\" href=\"${canonicalUrl}\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <meta name=\"description\" content=\"${description}\">\n <meta name=\"generator\" content=\"Scroll v${scrollVersion}\">\n <meta property=\"og:title\" content=\"${title}\">\n <meta property=\"og:description\" content=\"${description}\">\n <meta property=\"og:image\" content=\"${openGraphImage}\">\n ${faviconTag}\n ${gitTag}\n ${rssTag}\n <meta name=\"twitter:card\" content=\"summary_large_image\">\n </head>\n <body>`\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `<blockquote class=\"scrollQuote\">${this.subparticlesToString()}</blockquote>`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return `<meta http-equiv=\"Refresh\" content=\"0; url='${this.getAtom(1)}'\" />`\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.root.makeFullPath(this.content))\n }\n buildHtml() {\n return `<pre>${this.buildTxt()}</pre>`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n <script src=\"scripts/nested/hello.js\"></script>\n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `${name}.css`)\n }\n buildHtml() {\n return this.files.map(name => `<link rel=\"stylesheet\" type=\"text/css\" href=\"${name}\">`).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType anyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `</${this.closeTag}>` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nlinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an <a> tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cueFromId\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get attributes() {\n const attrs = [`href=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends linkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends linkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends linkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `<span class=\"scrollDateline\">${day} — </span>` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: `</${tag}>`, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom anyAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nchatLineParser\n popularity 0.009887\n catchAllAtomType anyAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(reduceParticle => {\n return {\n source: reduceParticle.getAtom(1),\n reduction: reduceParticle.getAtom(2),\n name: reduceParticle.getAtom(3) || reduceParticle.getAtomsFrom(1).join(\"_\")\n }\n })\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n if (reduction === \"concat\") {\n newRow[col.name] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[col.name] = group[0][sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[col.name] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom atomAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.getAtom(1)\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || untypedScalarValue === undefined) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return atom === \"\" || atom === undefined\n else if (operator === \"notEmpty\") return !(atom === \"\" || atom === undefined)\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n return this.getAtomsFrom(1)\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType stringAtom\n cue compose\n example\n table\n compose My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n catchAllAtomType stringAtom\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`<a href=\"${value.includes(\"@\") ? \"mailto:\" : \"\"}${value}\">${name}</a>`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.getAtom(1)\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom atomAtom\n catchAllAtomType atomAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType anyAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nscrollFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType scriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType anyAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nscrollFillParser\n cue fill\n extends abstractColumnNameParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nquoteLineParser\n popularity 0.004172\n catchAllAtomType anyAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return localStorage.getItem(fileOrUrl) || \"\"\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = localStorage.getItem(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n localStorage.setItem(url, await blob.text())\n return localStorage.getItem(url)\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `<a href=\"${this.editUrl}\" class=\"abstractTextLinkParser\">Edit</a>`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"161.0.4\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particle[methodName])\n .map((particle, index) => particle[methodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"<body>\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `<!DOCTYPE html>\\n<html lang=\"${this.lang}\">\\n${bodyTag}${content}\\n</body>\\n</html>` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll() {\n await this.load()\n await this.buildOne()\n await this.buildTwo()\n }\n async buildOne() {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne()\n }\n }\n async buildTwo(externalFilesCopied = {}) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(externalFilesCopied)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concept[measure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) row[measureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else row[measureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCache[measureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCache[measureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCache[measureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|footer$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"footer\")) {\n const pushes = particle.getParticles(\"footer\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` <item>\n <title>${title}</title>\n <link>${canonicalUrl}</link>\n <pubDate>${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}</pubDate>\n </item>`\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType anyAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n description Table from inline delimited data.\n catchAllAtomType anyAtom\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
\ No newline at end of file
+const AppConstants = {"parsers":"columnNameAtom\n extends stringAtom\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n extends stringAtom\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint comment\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n extends anyAtom\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\nscriptAnyAtom\n extends codeAtom\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom keywordAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint string\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\njavascriptAtom\n extends stringAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\"<a \") ? false : true\n return shouldLinkify ? this.replaceNotes(Utils.linkify(originalText)) : originalText\n }\n replaceNotes(originalText) {\n // Skip the replacements if there are no footnotes or the text has none.\n if (!this.root.footnotes.length || !originalText.includes(\"^\")) return originalText\n this.root.footnotes.forEach((note, index) => {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `<a href=\"#${note.htmlId}\" class=\"scrollNoteLink\" id=\"${linkBack}\"><sup>${note.label}</sup></a>`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.text}${this.closingTag}`\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return `</${tag}>`\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `<span class=\"scrollBlink\">${super.buildHtml()}</span>\n <script>setInterval(()=>{ Array.from(document.getElementsByClassName(\"scrollBlink\")).forEach(el => el.style.visibility = el.style.visibility === \"hidden\" ? \"visible\" : \"hidden\") }, 500)</script>`\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"linkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"</center>\")\n return `<center>${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser\n javascript\n compileSubparticles() {\n return this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildHtml() {\n return super.buildHtml() + this.compileSubparticles()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `<div style=\"text-indent:${(this.getIndentLevel() - 1) * 20}px;\"><input type=\"checkbox\" ${this.checked} id=\"${this.id}\"><label for=\"${this.id}\">` + super.text + `</label></div>`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `</${listType}>` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return `<style>\\n.${this.constructor.name} li::marker {content: \"${this.cue} \";}\\n</style>`\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* <span id=\"counter${id}\" title=\"0\">0</span><script>setInterval(()=>{ const el = document.getElementById('counter${id}'); el.title = parseFloat(el.title) + ${increment}; el.textContent = Math.floor(parseFloat(el.title)).toLocaleString()}, 100)</script> ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"</details>\")\n return `<details>${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `<a class=\"scrollFootNoteUsageLink\" href=\"#noteUsage${this.noteDefinitionIndex}\">${this.label}</a> ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"</div>\")\n return `<div class=\"scrollSection\">` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}></${this.tag}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return `<audio controls ${this.getAsHtmlAttributes(\"loop autoplay\".split(\" \"))}><source src=\"${this.filename}\" type=\"audio/mpeg\"></audio>`\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom\n heightParser\n cueFromId\n atoms cueAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\n widthParser\n // todo: fix inheritance bug\n cueFromId\n atoms cueAtom\n heightParser\n cueFromId\n atoms cueAtom\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* <span class=\"scrollStopwatchParser\" id=\"stopwatch${id}\">0.0</span><script>{let startTime = parseFloat(new URLSearchParams(window.location.search).get('start') || 0); document.getElementById('stopwatch${id}').title = startTime; setInterval(()=>{ const el = document.getElementById('stopwatch${id}'); el.title = parseFloat(el.title) + .1; el.textContent = (parseFloat(el.title)).toFixed(1)}, 100)}</script> `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"</div>\") // Single columns are self-closing after section break.\n return stackContents + `<div class=\"scrollColumns\" style=\"column-width:${columnWidth}ch;column-count:${maxColumns};max-width:${maxTotalWidth}ch;\">`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"</div>\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"</div>\")\n return `<style>.scrollContainerParser{width: 100%; box-sizing: border-box; max-width: ${this.maxWidth}; margin: 0 auto;}</style>` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `<div class=\"${this.defaultClass}\"><span>${this.dinkus}</span></div>`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n javascript\n buildHtml() {\n return `<hr>`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `<style>.abstractIconButtonParser {position:absolute;top:0.25rem; }.abstractIconButtonParser svg {fill: rgba(204,204,204,.8);width:1.875rem;height:1.875rem; padding: 0 7px;} .abstractIconButtonParser:hover svg{fill: #333;}</style><a href=\"${this.link}\" class=\"doNotPrint abstractIconButtonParser\" style=\"${this.style}\">${this.svg}</a>`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg <svg fill=\"#000000\" xmlns=\"http://www.w3.org/2000/svg\" width=\"800px\" height=\"800px\" viewBox=\"0 0 52 52\" enable-background=\"new 0 0 52 52\" xml:space=\"preserve\"><path d=\"M38.6,20.4c-1-6.5-6.7-11.5-13.5-11.5c-7.6,0-13.7,6.1-13.7,13.7c0,0.3,0,0.7,0.1,1c-5,0.4-8.9,4.6-8.9,9.6 c0,5.4,4.3,9.7,9.7,9.7h11.5c-0.8-0.8-8.1-8.1-8.1-8.1c-0.4-0.4-0.4-0.9,0-1.3l1.3-1.3c0.4-0.4,0.9-0.4,1.3,0l3.5,3.5 c0.4,0.4,1.1,0.1,1.1-0.4V21.8c0-0.4,0.5-0.9,1-0.9h1.9c0.5,0,0.9,0.4,0.9,0.9v13.4c0,0.6,0.8,0.8,1.1,0.4l3.5-3.5 c0.4-0.4,0.9-0.4,1.3,0l1.3,1.3c0.4,0.4,0.4,0.9,0,1.3L26,42.9h12.3v0c6.1-0.1,11-5.1,11-11.3C49.4,25.5,44.6,20.6,38.6,20.4z\"/></svg><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->\n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg <svg width=\"800px\" height=\"800px\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M21.1213 2.70705C19.9497 1.53548 18.0503 1.53547 16.8787 2.70705L15.1989 4.38685L7.29289 12.2928C7.16473 12.421 7.07382 12.5816 7.02986 12.7574L6.02986 16.7574C5.94466 17.0982 6.04451 17.4587 6.29289 17.707C6.54127 17.9554 6.90176 18.0553 7.24254 17.9701L11.2425 16.9701C11.4184 16.9261 11.5789 16.8352 11.7071 16.707L19.5556 8.85857L21.2929 7.12126C22.4645 5.94969 22.4645 4.05019 21.2929 2.87862L21.1213 2.70705ZM18.2929 4.12126C18.6834 3.73074 19.3166 3.73074 19.7071 4.12126L19.8787 4.29283C20.2692 4.68336 20.2692 5.31653 19.8787 5.70705L18.8622 6.72357L17.3068 5.10738L18.2929 4.12126ZM15.8923 6.52185L17.4477 8.13804L10.4888 15.097L8.37437 15.6256L8.90296 13.5112L15.8923 6.52185ZM4 7.99994C4 7.44766 4.44772 6.99994 5 6.99994H10C10.5523 6.99994 11 6.55223 11 5.99994C11 5.44766 10.5523 4.99994 10 4.99994H5C3.34315 4.99994 2 6.34309 2 7.99994V18.9999C2 20.6568 3.34315 21.9999 5 21.9999H16C17.6569 21.9999 19 20.6568 19 18.9999V13.9999C19 13.4477 18.5523 12.9999 18 12.9999C17.4477 12.9999 17 13.4477 17 13.9999V18.9999C17 19.5522 16.5523 19.9999 16 19.9999H5C4.44772 19.9999 4 19.5522 4 18.9999V7.99994Z\"/></svg>\n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg <svg viewBox=\"3 5 24 20\" width=\"24\" height=\"20\" xmlns=\"http://www.w3.org/2000/svg\"><g transform=\"matrix(1, 0, 0, 1, 0, -289.0625)\"><path style=\"opacity:1;stroke:none;stroke-width:0.49999997;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1\" d=\"M 5 5 C 4.2955948 5 3.6803238 5.3628126 3.3242188 5.9101562 L 14.292969 16.878906 C 14.696939 17.282876 15.303061 17.282876 15.707031 16.878906 L 26.675781 5.9101562 C 26.319676 5.3628126 25.704405 5 25 5 L 5 5 z M 3 8.4140625 L 3 23 C 3 24.108 3.892 25 5 25 L 25 25 C 26.108 25 27 24.108 27 23 L 27 8.4140625 L 17.121094 18.292969 C 15.958108 19.455959 14.041892 19.455959 12.878906 18.292969 L 3 8.4140625 z \" transform=\"translate(0,289.0625)\" id=\"rect4592\"/></g></svg>\n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg <svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12.7166 3.79541C12.2835 3.49716 11.7165 3.49716 11.2834 3.79541L4.14336 8.7121C3.81027 8.94146 3.60747 9.31108 3.59247 9.70797C3.54064 11.0799 3.4857 13.4824 3.63658 15.1877C3.7504 16.4742 4.05336 18.1747 4.29944 19.4256C4.41371 20.0066 4.91937 20.4284 5.52037 20.4284H8.84433C8.98594 20.4284 9.10074 20.3111 9.10074 20.1665V15.9754C9.10074 14.9627 9.90433 14.1417 10.8956 14.1417H13.4091C14.4004 14.1417 15.204 14.9627 15.204 15.9754V20.1665C15.204 20.3111 15.3188 20.4284 15.4604 20.4284H18.4796C19.0806 20.4284 19.5863 20.0066 19.7006 19.4256C19.9466 18.1747 20.2496 16.4742 20.3634 15.1877C20.5143 13.4824 20.4594 11.0799 20.4075 9.70797C20.3925 9.31108 20.1897 8.94146 19.8566 8.7121L12.7166 3.79541ZM10.4235 2.49217C11.3764 1.83602 12.6236 1.83602 13.5765 2.49217L20.7165 7.40886C21.4457 7.91098 21.9104 8.73651 21.9448 9.64736C21.9966 11.0178 22.0564 13.5119 21.8956 15.3292C21.7738 16.7067 21.4561 18.4786 21.2089 19.7353C20.9461 21.0711 19.7924 22.0001 18.4796 22.0001H15.4604C14.4691 22.0001 13.6655 21.1791 13.6655 20.1665V15.9754C13.6655 15.8307 13.5507 15.7134 13.4091 15.7134H10.8956C10.754 15.7134 10.6392 15.8307 10.6392 15.9754V20.1665C10.6392 21.1791 9.83561 22.0001 8.84433 22.0001H5.52037C4.20761 22.0001 3.05389 21.0711 2.79113 19.7353C2.54392 18.4786 2.22624 16.7067 2.10437 15.3292C1.94358 13.5119 2.00338 11.0178 2.05515 9.64736C2.08957 8.73652 2.55427 7.91098 3.28346 7.40886L10.4235 2.49217Z\"/></svg>\n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg <svg xmlns=\"http://www.w3.org/2000/svg\" direction=\"ltr\" width=\"231.52568231268793\" height=\"249.33156169975996\" viewBox=\"297.27169239534896 1273.121785992385 231.52568231268793 249.33156169975996\" stroke-linecap=\"round\" stroke-linejoin=\"round\" data-color-mode=\"dark\" class=\"tl-container tl-theme__force-sRGB tl-theme__dark\" ><defs/><g transform=\"matrix(1, 0, 0, 1, 395.9682, 1413.3618)\" opacity=\"1\"><g transform=\"scale(1)\"><path d=\"M-5.7342,-1.1484 T-5.0182,-4.6536 -2.6672,-12.9768 1.7374,-22.3903 8.4597,-30.0892 16.5455,-35.2796 25.3478,-37.8247 34.4641,-37.2199 43.0676,-33.4606 50.27,-27.1118 55.7861,-19.7972 59.8042,-11.6764 61.6844,-2.3157 60.8049,7.9615 57.3259,17.5954 51.2605,25.5515 41.9451,31.9237 30.6489,36.2084 18.9812,37.6038 8.4915,36.637 -1.3063,34.5174 -10.6869,31.6604 -19.0463,27.0252 -26.0612,20.1562 -31.864,11.1271 -35.2772,1.0066 -35.7963,-9.1933 -35.203,-19.2337 -32.52,-28.3259 -27.3388,-37.4245 -20.2857,-45.7085 -11.787,-53.0395 -2.4314,-58.4864 7.1724,-61.8739 17.5002,-65.6025 28.2859,-68.1034 38.514,-69.1081 48.1844,-69.9134 57.9136,-70.1787 67.4056,-69.5971 76.297,-67.7446 84.5233,-63.97 92.0158,-57.5003 98.1976,-48.5614 102.8839,-38.056 105.9936,-27.6097 107.074,-18.168 107.175,-8.2882 106.6452,1.9768 105.4734,11.7698 104.0279,21.1013 101.7439,31.0445 98.0977,40.6386 92.7822,49.1039 86.2847,57.2337 78.6099,64.7113 69.8339,70.974 61.2863,75.092 52.4283,78.2186 42.5338,80.7945 32.7733,82.6629 22.8568,83.6574 12.2803,83.3538 2.4971,81.4828 -6.3367,78.789 -15.1061,75.8011 -24.259,72.3779 -33.2286,68.991 -41.5707,64.8363 -49.153,59.1909 -55.8375,52.2938 -62.9311,43.9553 -68.4604,34.4882 -71.2761,23.5855 -72.748,12.7452 -73.7673,2.0453 -74.1187,-8.7386 -72.293,-19.3856 -68.7376,-30.4053 -64.12,-39.7852 -59.1825,-47.8931 -53.7695,-56.4512 -48.1998,-65.7676 -43.1389,-74.401 -36.9006,-81.0179 -28.4317,-86.2639 -18.3342,-90.3521 -8.9259,-93.9355 0.1927,-98.7179 9.3551,-103.3537 18.7651,-106.7552 29.7482,-110.3584 40.9287,-113.446 50.87,-115.2231 57.8279,-116.0074 60.4065,-116.0632 A7.8326,7.8326 0 0 1 61.1735,-100.4168 T58.6018,-100.2201 51.1634,-99.3008 41.695,-96.884 32.4044,-94.0187 22.65,-91.382 13.4974,-87.3756 5.0519,-83.1026 -5.0443,-78.6175 -15.7236,-74.4743 -24.6193,-70.2681 -31.274,-62.5903 -36.3827,-53.9362 -41.2833,-46.2905 -46.0679,-38.5161 -50.639,-30.6576 -54.6876,-22.4063 -57.6038,-12.3346 -58.5104,-1.8678 -57.6022,9.4969 -56.0628,20.477 -53.6434,29.0487 -48.8908,36.7517 -42.4075,43.9784 -35.3064,50.6236 -27.3553,55.2062 -17.6884,59.4064 -7.6442,63.6638 2.4427,67.0449 12.7308,69.4843 22.9294,70.2171 33.2547,69.3921 42.4972,67.7384 52.2753,64.8425 61.8649,60.9142 69.5292,56.0828 76.3604,49.5927 82.431,42.3194 87.219,34.619 90.6076,25.5217 92.547,16.0352 93.8703,7.1315 94.4972,-2.5268 94.3672,-13.3681 93.3204,-24.4252 90.5377,-34.1656 85.7321,-43.6441 78.7334,-51.8057 68.7542,-56.0559 57.9857,-57.2713 48.079,-57.0198 38.4535,-56.3204 28.5246,-55.0036 18.6104,-52.4796 9.6402,-49.559 0.7193,-46.2981 -7.1531,-41.3715 -13.708,-34.8957 -19.5435,-26.853 -22.8356,-17.0622 -23.363,-6.5446 -21.9969,3.1439 -17.1086,11.9113 -9.276,18.744 -0.0685,22.6058 9.3636,24.796 19.2422,25.8817 29.052,25.0411 37.9338,21.3502 45.6819,13.8498 49.6675,4.5228 49.2802,-5.1854 45.3277,-14.4455 38.8603,-21.7879 30.5488,-25.7753 21.3356,-24.3975 13.6687,-19.2391 8.8217,-10.9919 6.423,-2.3609 5.7342,1.1484 A5.8481,5.8481 0 0 1 -5.7342,-1.1484 Z\" stroke-linecap=\"round\"/></g></g></svg>\n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return `<div class=\"abstractTextLinkParser\"><a href=\"${this.link}\">${this.text}</a></div>`\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n <script>\n sendFormViaEmail = form => {\n const mailto = new URL(\"mailto:\")\n const params = []\n const { value, title } = form.querySelector('button[type=\"submit\"]')\n params.push(`subject=${encodeURIComponent(value)}`)\n params.push(`to=${encodeURIComponent(title)}`)\n const oneTextarea = form.querySelector('textarea[title=\"oneTextarea\"]')\n const body = oneTextarea ? codeMirrorInstance.getValue() : Array.from(new FormData(form)).map(([name, value]) => `${name} ${value}`).join(\"\\\\n\")\n params.push(`body=${encodeURIComponent(body)}`)\n mailto.search = params.join(\"&\")\n window.open(mailto.href, '_blank')\n }\n </script>\n string style\n <style> .scrollFormParser {\n font-family: \"Gill Sans\", \"Bitstream Vera Sans\", sans-serif;\n }\n .scrollFormParser input , .scrollFormParser textarea{\n padding: 10px;\n margin-bottom: 10px;\n width: 100%;\n box-sizing: border-box;\n } .scrollFormParser label {\n display: block;\n margin-bottom: 5px;\n }\n </style>\n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = `<textarea placeholder=\"${placeholder}\" id=\"${Name}\" name=\"${Name}\" ${IsRequired ? \"required\" : \"\"}></textarea>`\n else\n tag = `<input placeholder=\"${placeholder}\" type=\"${type}\" id=\"${Name}\" name=\"${Name}\" ${IsRequired ? \"required\" : \"\"}>`\n return `<div><label for=\"${Name}\" title=\"${IsRequired ? \"Required\" : \"\"}\">${ucFirst}${IsRequired ? \"*\" : \"\"}:</label>${tag}</div>`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}<form ${isEmail ? \"onsubmit='sendFormViaEmail(this); return false;'\" : ` method='post' action='${formDestination}'`} class=\"scrollFormParser\">${this.inputs}<button value=\"${subject}\" title=\"${formDestination}\" class=\"scrollButton\" type=\"submit\">${callToAction}</button>${this.footer}</form>`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal codeMirror.css scrollLibs.js constants.js\n string requireOnce\n <link rel=\"stylesheet\" href=\"codeMirror.css\">\n <script src=\"scrollLibs.js\"></script>\n <script src=\"constants.js\"></script>\n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `<textarea title=\"oneTextarea\" rows=\"${Math.min(this.root.measures.length * 2, 30)}\" placeholder=\"${this.placeholder}\" id=\"${Name}\" name=\"${Name}\"></textarea>\n <script id=\"${Name}Parsers\" type=\"text/plain\">${this.parsersBundle}</script>\n <script>{\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n let {width, height} = document.getElementById('${Name}').getBoundingClientRect();\n const sp = new Particle(AppConstants.parsers)\n sp.filter(particle => particle.getLine().match(parserRegex)).forEach(part => {\n part.appendLine(\"boolean suggestInAutocomplete false\")\n })\n const customScrollParser = new HandParsersProgram(sp.toString() + document.getElementById(\"${Name}Parsers\").textContent).compileAndReturnRootParser()\n codeMirrorInstance = new ParsersCodeMirrorMode(\"custom\", () => customScrollParser, undefined, CodeMirror).register().fromTextAreaWithAutocomplete(document.getElementById(\"${Name}\"), {\n lineWrapping: false,\n lineNumbers: false\n })\n codeMirrorInstance.setSize(width, height);\n codeMirrorInstance.setValue(\\`${this.value}\\`); }</script>`\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `<a class=\"scrollContinueReadingLink\" href=\"${linkRelativeToCompileTarget}\">Continue reading...</a>`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `<div class=\"scrollSnippetContainer\">${this.makeSnippet(file.file.scrollProgram, buildSettings)}</div>`\n }).join(\"\\n\\n\")\n return `<div class=\"scrollColumns\" style=\"column-width:35ch;\">${snippets}</div>`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue nav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return `<nav class=\"scrollNavParser\">` + this.root.getFilesByTags(this.content).map(file => {\n const { linkTitle, permalink } = file.file.scrollProgram\n return `<a href=\"${permalink}\">${linkTitle}</a>` \n }).join(this.get(\"join\") || \" \") + `</nav>`\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `<div><a href=\"${permalink}\">${title}</a><div>${description}...</div><div class=\"subdued\" style=\"text-align:right;\">${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}</div></div>`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `<div id=\"${id}\" class=\"scrollNoticesParser\" style=\"display:none;\"></div><script>(function(){\n const params = new URLSearchParams(window.location.search)\n if (!params.size) return\n document.getElementById('${id}').innerHTML = Array.from(params.entries()).map(([key, value]) => {\n if (key === \"error\") \n return '<div style=\"color:red\">Error: ' + value + '</div>'\n if (key === \"success\")\n return '<div style=\"color:green\">Success: ' + value + '</div>'\n return '<div>' + key + ': ' + value + '</div>'\n }).join(\"\") || \"No query parameters found\"\n document.getElementById('${id}').style.display = \"block\"\n })()</script>`\n }\nprintSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.buildTxt().replace(/\\</g, \"<\")}</code>`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual} = this\n const expected = this.subparticlesToString()\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType anyAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n javascript\n buildHtml() {\n return `<div class=\"printDateParser\">${this.day}</div>`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne() { await this._buildFileType(this.extension) }\nbuildCsvParser\n popularity 0.000096\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo() {\n await this._buildFileType(this.extension)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(externalFilesCopied) {\n await this._copyExternalFiles(externalFilesCopied)\n await super.buildTwo()\n }\n async _copyExternalFiles(externalFilesCopied = {}) {\n if (!this.isNodeJs()) return\n const {root} = this\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Write RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Write concepts to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom anyAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildCommandParser\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Write measures to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom anyAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo() {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${this.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the <html lang=\"\"> tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Meta tag description.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType anyAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType anyAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `<div style=\"text-align: ${index % 2 ? \"right\" : \"left\"};\" class=\"scrollChat ${index % 2 ? \"scrollChatRight\" : \"scrollChatLeft\"}\"><span>${line.asString}</span></div>` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = typeof d3 === \"undefined\" ? require('d3') : d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = typeof d3 === \"undefined\" ? require('d3') : d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/</g, \"<\")\n return {\n title, titleLink: relativePath + permalink, text, date, wordCount, minutes\n }\n }\n columnNames = \"title titleLink text date wordCount minutes\".split(\" \")\nscrollPostsParser\n popularity 0.000024\n cue posts\n description Posts as table.\n extends abstractPostsParser\n example\n // Print a search table:\n posts\n printTable\n tableSearch\n // Dump a CSV of blog:\n buildHtml\n buildCsv\n buildJson\nscrollPostsMetaParser\n popularity 0.000024\n cue postsMeta\n description Post meta as table.\n extends abstractPostsParser\n javascript\n columnNames = [\"date\", \"year\", \"title\", \"permalink\", \"authors\", \"tags\", \"wordCount\", \"minutes\"]\n postToRow(file) {\n const {date, year, title, permalink, authors, tags, wordCount, minutes} = file.file.scrollProgram\n return {\n date, year, title, permalink, authors, tags, wordCount, minutes\n }\n }\nprintFeedParser\n popularity 0.000048\n description Print group to RSS.\n extends abstractPostsParser\n example\n printFeed index\n printFeed cars/index\n buildRss\n javascript\n buildRss() {\n const {dayjs} = this.root\n const scrollPrograms = this.files.map(file => file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>\n <rss version=\"2.0\">\n <channel>\n <title>${title}</title>\n <link>${baseUrl}</link>\n <description>${description}</description>\n <lastBuildDate>${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}</lastBuildDate>\n <language>en-us</language>\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n </channel>\n </rss>`\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.code.replace(/\\</g, \"<\")}</code>`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `<div class=\"codeWithHeader\"><div class=\"codeHeader\">${this.content}</div>${super.buildHtml()}</div>`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n <script>\n document.addEventListener(\"DOMContentLoaded\", () => document.querySelectorAll(\".scrollCodeBlock\").forEach(block =>\n {\n if (!navigator.clipboard) return\n const button = document.createElement(\"span\")\n button.classList.add(\"scrollCopyButton\")\n block.appendChild(button)\n button.addEventListener(\"click\", async () => {\n await navigator.clipboard.writeText(block.innerText)\n button.classList.add(\"scrollCopiedButton\")\n })\n }\n ))\n </script>\nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = `<style>\n .heatrixContainer {\n margin: auto;\n }.heatrixRow {white-space: nowrap;}\n ${cssId} .heatrixAtom {\n font-family: arial;\n border-radius: 2px;\n border: 1px solid transparent;\n display: inline-block;\n margin: 1px;\n text-align: center;\n vertical-align: middle;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n .heatrixAtom a {\n color: black;\n }\n ${cssId} .heatrixAtom{\n width: ${defaultWidth};\n height: ${defaultHeight};\n font-size: ${fontSize};\n line-height: ${lineHeight};\n }\n </style>`\n const firstRow = this.table[0]\n return (\n `<div class=\"heatrixContainer\" id=\"heatrix${this.uid}\">${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `<div class=\"heatrixRow heatrixRow${rowIndex}\">${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return `<div class=\"heatrixAtom heatrixColumn${columnIndex}${valueClass}\" style=\"${inlineStyle}\"><a title=\"${label}\" ${href}>${label}</a></div>`\n })\n .join(\"\")}</div>`\n })\n .join(\"\\n\") +\n \"</div>\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom anyAtom\n cueFromId\n single\n colorParser\n atoms cueAtom anyAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType anyAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal leaflet.css leaflet.js scrollLibs.js\n string requireOnce\n <link rel=\"stylesheet\" href=\"leaflet.css\">\n <script src=\"leaflet.js\"></script>\n <script src=\"scrollLibs.js\"></script>\n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `<div id=\"${mapId}\" style=\"${style}\"></div>\n <script>\n {\n if (!window.maps) window.maps = {}\n const moveToMyLocation = () => {\n if (!navigator.geolocation) return\n navigator.geolocation.getCurrentPosition((position) => {\n const { latitude, longitude } = position.coords\n window.maps.${mapId}.setView([latitude, longitude])\n L.circleMarker([latitude, longitude], {\n fillOpacity: 0.8,\n radius: 20,\n weight: 4\n })\n .addTo(window.maps.${mapId})\n }, () => {})\n }\n const lat = ${this.get(\"lat\") ?? 37.8}\n const long = ${this.get(\"long\") ?? 4}\n if (${this.has(\"geolocate\")}){\n moveToMyLocation()\n }\n const zoomLevel = ${this.get(\"zoom\") ?? 4}\n const hover = '${this.get(\"hover\") || \"<b>{title}</b><br>{description}\"}'\n const template = ${JSON.stringify(template)}\n const points = ${JSON.stringify((this.parent.coreTable || []).filter(point => point.lat && point.long), undefined, 2)}\n window.maps.${mapId} = L.map(\"map${id}\").setView([lat, long], zoomLevel)\n const map = window.maps.${mapId}\n const tileOptions = {\n \"default\": {\n baseLayer: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',\n attribution: '<a href=\"https://www.openstreetmap.org/\">OpenStreetMap</a> contributors'\n },\n light: {\n baseLayer: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',\n attribution: '<a href=\"https://www.openstreetmap.org/\">OpenStreetMap</a> contributors <a href=\"https://carto.com/ attributions\">CARTO</a>'\n },\n }\n const {baseLayer, attribution} = tileOptions.${this.get(\"tiles\") || \"default\"}\n L.tileLayer(baseLayer, {\n attribution,\n maxZoom: 19\n }).addTo(map);\n points.forEach(point => {\n L.circleMarker([point.lat, point.long], {...template, ...Object.fromEntries(\n Object.entries(point).filter(([key, value]) => value !== null)\n )})\n .addTo(map)\n .bindPopup(new Particle(point).evalTemplateString(hover))\n })\n }\n </script>`\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal d3.js plot.js\n string requireOnce\n <script src=\"d3.js\"></script>\n <script src=\"plot.js\"></script>\n example\n plot\n inScope abstractColumnNameParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `<div id=\"${id}\"></div><script>\n {\n let loadChart = async () => {\n const data = ${this.dataCode}\n const get = (col, index ) => col !== \"undefined\" ? col : (index === undefined ? undefined : Object.keys(data[0])[index])\n document.querySelector(\"#${id}\").append(Plot.plot(${this.plotOptions}))\n }\n loadChart()\n }\n </script>`\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n }`\n }\nscatterplotParser\n extends abstractPlotParser\n description Scatterplot Widget.\n // todo: make copyFromExternal work with inheritance\n string copyFromExternal d3.js plot.js\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true,\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal sparkline.js\n string requireOnce <script src=\"sparkline.js\"></script>\n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return `<span id=\"${id}\"></span><script>new Sparkline(document.getElementById(\"${id}\"), {dotRadius: 0, width: ${width}, height: ${height}, lineColor: \"${lineColor}\", tooltip: (value,index) => ${start} + index + \": \" + value}).draw(${JSON.stringify(columnValues)})</script>`\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `<th>${column.name}</th>\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `<a href=\"${link}\">${text}</a>`\n else if (col.name.endsWith(\"Url\")) tagged = `<a href=\"${content}\">${col.name.replace(\"Url\", \"\")}</a>`\n else if (isUrl) tagged = `<a href=\"${content}\">${text}</a>`\n str += `<td>${tagged}</td>\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `<tr>${this.toRow(row)}</tr>`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `<table id=\"table${this._getUid()}\" class=\"scrollTable\">\n <thead><tr>${this.tableHeader.join(\"\\n\")}</tr></thead>\n <tbody>${this.tableBody}</tbody>\n </table>`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal katex.min.css katex.min.js\n string requireOnce\n <link rel=\"stylesheet\" href=\"katex.min.css\">\n <script defer src=\"katex.min.js\"></script>\n <script>\n document.addEventListener(\"DOMContentLoaded\", () => document.querySelectorAll(\".scrollKatex\").forEach(el =>\n {\n katex.render(el.innerText, el, {\n throwOnError: false\n });\n }\n ))\n </script>\n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `<div class=\"scrollKatex\" id=\"${id}\">${content + this.subparticlesToString()}</div>`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `<style>#helpfulNotFound{margin: 100px 0;}</style><h1 id=\"helpfulNotFound\"></h1><script defer src=\"/helpfulNotFound.js\"></script><script>document.addEventListener(\"DOMContentLoaded\", () => new NotFoundApp('${this.content}'))</script>`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal jquery-3.7.1.min.js slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `<style>html {font-size: var(--scrollBaseFontSize, 28px);} body {margin: auto; width: 500px;}.slideshowNav{text-align: center; margin-bottom:20px; font-size: 24px;color: rgba(204,204,204,.8);} a{text-decoration: none; color: rgba(204,204,204,.8);}</style><script defer src=\"jquery-3.7.1.min.js\"></script><div class=\"slideshowNav\"></div><script defer src=\"slideshow.js\"></script>`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal jquery-3.7.1.min.js datatables.css dayjs.min.js datatables.js tableSearch.js\n string requireOnce\n <script defer src=\"jquery-3.7.1.min.js\"></script>\n <style>.dt-search{font-family: \"SF Pro\", \"Helvetica Neue\", \"Segoe UI\", \"Arial\";}</style>\n <link rel=\"stylesheet\" href=\"datatables.css\">\n <script defer src=\"datatables.js\"></script>\n <script defer src=\"dayjs.min.js\"></script>\n <script defer src=\"tableSearch.js\"></script>\n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>${this.css}</style>`\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCs() {\n return this.css\n }\ninlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractScrollParser\n cueFromId\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return `<style>/* ${this.content} */\\n${this.css}</style>`\n }\n get css() {\n return this.atoms.slice(1).map(filename => this.root.readFile(filename)).join(\"\\n\\n\")\n }\n buildCs() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>html, body { background: ${this.content};}</style>`\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>html, body { color: ${this.content};}</style>`\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return `<style>html, body, h1,h2,h3 { font-family: ${font};}</style>`\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return `<link rel=\"stylesheet\" type=\"text/css\" href=\"${this.filename}\">`\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return `<script src=\"${this.filename}\"></script>`\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `<tr>` + items.map(particle => `<td>${particle.cue}<span>${particle.content}</span></td>`).join(\"\\n\") + `</tr>\\n`\n }\n buildHtml() {\n return `<table class=\"scrollDashboard\">${this.tableBody}</table>`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = this[method]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = next[method]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.code.replace(/\\</g, \"<\")}</code>`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType anyAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\ninspectBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal inspector.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `<div class=\"inspectorParticle\"><span class=\"inspectorParticleId\">${particle.constructor.name}</span>${particle.atoms.map((atom, index) => `<span class=\"inspectorAtom\">${atom}<span class=\"inspectorAtomType\">${atomTypes[index]}</span></span>`).join(\" \")}${(particle.length ? `<br><div class=\"inspectorSubparticles\">` + particle.map(mapFn).join(\"<br>\") + `</div>` : \"\")}</div>`}\n return this.selectedParticles.map(mapFn).join(\"<br>\")\n }\n buildHtml() {\n return `<link rel=\"stylesheet\" href=\"inspector.css\">` + this.code\n }\ninspectAboveParser\n description Inspect particle above.\n extends inspectBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return `<style>${this.css}</style>`\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCs() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(`</${tag}>`)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `<br>`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => `<iframe src=\"${url}\" frameborder=\"0\"></iframe>`).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `<figcaption>${caption.buildHtml()}</figcaption>` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `<figure class=\"scrollCaptionedFigure\" style=\"${widthStyle + float}\">${this.getFigureContent(buildSettings)}${captionFig}</figure>`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser linkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"linkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return `<a href=\"${clickLink}\" target=\"${target}\" ${className} ${id}><img src=\"${linkRelativeToCompileTarget}\" ${dimensionAttributes}loading=\"lazy\"></a>`\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \"qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `<div class=\"scrollYouTubeHolder\"><iframe class=\"scrollYouTubeEmbed\" src=\"${url}\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe></div>`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\ninlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractScrollParser\n cueFromId\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return `<script>/* ${this.content} */\\n${this.contents}</script>`\n }\n get contents() {\n return this.atoms.slice(1).map(filename => this.root.readFile(filename)).join(\";\\n\\n\")\n }\n buildJs() {\n return this.contents\n }\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType scriptAnyAtom\n javascript\n buildHtml() {\n return `<script>${this.scriptContent}</script>`\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return `<script>window.${varName} = ${this.root.readFile(this.filename)}</script>`\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<style>${style}</style><a class=\"keyboardNav doNotPrint\" style=\"left:.5rem;\" href=\"${linkToPrevious}\"><</a><a class=\"keyboardNav doNotPrint\" style=\"right:.5rem;\" href=\"${linkToNext}\">></a>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = `<script>document.addEventListener('keydown', function(event) {\n if (document.activeElement !== document.body) return\n if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return // dont interfere with keyboard back button shortcut\n const getLinks = () => document.getElementsByClassName(\"scrollKeyboardNav\")[0].getElementsByTagName(\"a\")\n if (event.key === \"ArrowLeft\")\n getLinks()[0].click()\n else if (event.key === \"ArrowRight\")\n getLinks()[1].click()\n });</script>`\n return `<div class=\"scrollKeyboardNav\" style=\"display:none;\"><a href=\"${linkToPrevious}\">${linkToPrevious}</a> · ${root.permalink} · <a href=\"${linkToNext}\">${linkToNext}</a>${script}</div>`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"<h></h>\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `<style>${css}</style><div id=\"scrollLeetSheet\">` + doc.map(obj => `<div class=\"${obj.category}\"><a href=\"${this.makeLink(obj.examples, obj.id)}\">${obj.isPopular ? \"<b>\" : \"\"}${obj.id}</a> ${obj.description}${obj.isPopular ? \"</b>\" : \"\"}${obj.note}</div>`).join(\"\\n\") + \"</div>\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n buildCsv() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category} = obj\n return {\n id,\n isPopular,\n description,\n popularity,\n category\n }\n })\n return new Particle(this.root.lodash.sortBy(rows, \"isPopular\")).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"<p><b>Parser Definition Parsers</b> define parsers that acquire, analyze and act on code.</p>\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"<p><b>Atom Definition Parsers</b> analyze the atoms in a line.</p>\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` <span class=\"note\">A${category.replace(\"Phase\", \"\").substr(1)}Time.</span>`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `<link rel=\"icon\" href=\"${favicon}\">` : \"\"\n const rssTag = rssFeedUrl ? `<link rel=\"alternate\" type=\"application/rss+xml\" title=\"${title}\" href=\"${rssFeedUrl}\">` : \"\"\n const gitTag = gitRepo ? `<link rel=\"source\" type=\"application/git\" title=\"Source Code Repository\" href=\"${gitRepo}\">` : \"\"\n return `<head>\n <meta charset=\"utf-8\">\n <title>${title}</title>\n <script>/* This HTML was generated by 📜 Scroll v${scrollVersion}. https://scroll.pub */</script>\n <style>@media print {.doNotPrint {display: none !important;}}</style>\n <link rel=\"canonical\" href=\"${canonicalUrl}\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <meta name=\"description\" content=\"${description}\">\n <meta name=\"generator\" content=\"Scroll v${scrollVersion}\">\n <meta property=\"og:title\" content=\"${title}\">\n <meta property=\"og:description\" content=\"${description}\">\n <meta property=\"og:image\" content=\"${openGraphImage}\">\n ${faviconTag}\n ${gitTag}\n ${rssTag}\n <meta name=\"twitter:card\" content=\"summary_large_image\">\n </head>\n <body>`\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `<blockquote class=\"scrollQuote\">${this.subparticlesToString()}</blockquote>`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return `<meta http-equiv=\"Refresh\" content=\"0; url='${this.getAtom(1)}'\" />`\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.content)\n }\n buildHtml() {\n return `<pre>${this.buildTxt()}</pre>`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n //if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n <script src=\"scripts/nested/hello.js\"></script>\n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `${name}.css`)\n }\n buildHtml() {\n return this.files.map(name => `<link rel=\"stylesheet\" type=\"text/css\" href=\"${name}\">`).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType anyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `</${this.closeTag}>` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nlinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an <a> tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cueFromId\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get attributes() {\n const attrs = [`href=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends linkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends linkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends linkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `<span class=\"scrollDateline\">${day} — </span>` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: `</${tag}>`, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom anyAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nchatLineParser\n popularity 0.009887\n catchAllAtomType anyAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(reduceParticle => {\n return {\n source: reduceParticle.getAtom(1),\n reduction: reduceParticle.getAtom(2),\n name: reduceParticle.getAtom(3) || reduceParticle.getAtomsFrom(1).join(\"_\")\n }\n })\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n if (reduction === \"concat\") {\n newRow[col.name] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[col.name] = group[0][sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[col.name] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom atomAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.getAtom(1)\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || untypedScalarValue === undefined) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return atom === \"\" || atom === undefined\n else if (operator === \"notEmpty\") return !(atom === \"\" || atom === undefined)\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n return this.getAtomsFrom(1)\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType stringAtom\n cue compose\n example\n table\n compose My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n catchAllAtomType stringAtom\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`<a href=\"${value.includes(\"@\") ? \"mailto:\" : \"\"}${value}\">${name}</a>`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.getAtom(1)\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom atomAtom\n catchAllAtomType atomAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType anyAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nscrollFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType scriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType anyAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nscrollFillParser\n cue fill\n extends abstractColumnNameParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nquoteLineParser\n popularity 0.004172\n catchAllAtomType anyAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return localStorage.getItem(fileOrUrl) || \"\"\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = localStorage.getItem(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n localStorage.setItem(url, await blob.text())\n return localStorage.getItem(url)\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `<a href=\"${this.editUrl}\" class=\"abstractTextLinkParser\">Edit</a>`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"161.3.0\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particle[methodName])\n .map((particle, index) => particle[methodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"<body>\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `<!DOCTYPE html>\\n<html lang=\"${this.lang}\">\\n${bodyTag}${content}\\n</body>\\n</html>` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll() {\n await this.load()\n await this.buildOne()\n await this.buildTwo()\n }\n async buildOne() {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne()\n }\n }\n async buildTwo(externalFilesCopied = {}) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(externalFilesCopied)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concept[measure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) row[measureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else row[measureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCache[measureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCache[measureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCache[measureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|footer$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n try {\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n } catch (err) {\n console.error(err)\n }\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"footer\")) {\n const pushes = particle.getParticles(\"footer\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` <item>\n <title>${title}</title>\n <link>${canonicalUrl}</link>\n <pubDate>${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}</pubDate>\n </item>`\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType anyAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n description Table from inline delimited data.\n catchAllAtomType anyAtom\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
\ No newline at end of file
diff --git a/package.json b/package.json
index c9a8fbf..7194604 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
},
"dependencies": {
"jquery": "^3.6.0",
- "scroll-cli": "^161.2.0",
+ "scroll-cli": "^161.3.0",
"scrollsdk": "^99.2.0"
},
"devDependencies": {
diff --git a/scroll.parsers b/scroll.parsers
index 745d63e..d901a5f 100644
--- a/scroll.parsers
+++ b/scroll.parsers
@@ -3841,7 +3841,7 @@ toStampParser
cueFromId
javascript
buildTxt() {
- return this.makeStamp(this.root.makeFullPath(this.content))
+ return this.makeStamp(this.content)
}
buildHtml() {
return `<pre>${this.buildTxt()}</pre>`
@@ -3862,7 +3862,7 @@ toStampParser
items.forEach(item => {
const itemPath = path.join(currentPath, item);
const relativePath = path.relative(dir, itemPath);
- if (!gitTrackedFiles.has(item)) return
+ //if (!gitTrackedFiles.has(item)) return
const stats = fs.statSync(itemPath);
const indentation = ' '.repeat(depth);
if (stats.isDirectory()) {
@@ -5647,7 +5647,7 @@ scrollParser
}
get scrollVersion() {
// currently manually updated
- return "161.0.4"
+ return "161.3.0"
}
// Use the first paragraph for the description
// todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)
@@ -6070,10 +6070,14 @@ scrollParser
.forEach(particle => {
let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(" ")
const kind = particle.cue
+ try {
if (kind === "replaceJs") value = eval(value)
if (this.isNodeJs() && kind === "replaceNodejs")
this.evalNodeJsMacros(value, macroMap, absolutePath)
else macroMap[particle.getAtom(1)] = value
+ } catch (err) {
+ console.error(err)
+ }
particle.destroy() // Destroy definitions after eval
})
if (particle.has("footer")) {
------------------------------------------------------------
commit dec7bd1a8380cbfc3335786fbdb96bbeefe343a7
Author: Breck Yunits <breck7@gmail.com>
Date: Wed Dec 4 21:25:01 2024 -1000
diff --git a/package.json b/package.json
index 0940f33..c9a8fbf 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
},
"dependencies": {
"jquery": "^3.6.0",
- "scroll-cli": "^161.1.0",
+ "scroll-cli": "^161.2.0",
"scrollsdk": "^99.2.0"
},
"devDependencies": {
------------------------------------------------------------
commit 96a440301732e6b5c921eb18d2e224871ae99c69
Author: Breck Yunits <breck7@gmail.com>
Date: Wed Dec 4 07:08:34 2024 -1000
diff --git a/dist/app.js b/dist/app.js
index a1c9d35..65c9f98 100644
--- a/dist/app.js
+++ b/dist/app.js
@@ -47,14 +47,9 @@ class CodeEditorComponent extends AbstractParticleComponentParser {
rehighlight() {
if (this._parser === this.root.parser) return
- console.log("rehighlighting")
+ console.log("rehighlighting needed")
this._parser = this.root.parser
-
- const editor = this.codeMirrorInstance
- setTimeout(() => {
- const content = editor.getValue()
- editor.setValue(content)
- }, 1000) // Use a timeout to ensure rendering happens
+ // todo: figure this out. codemirror seems to not want to repaint.
}
codeWidgets = []
------------------------------------------------------------
commit cdc331c0d8d37c3de8e0f90590fda6e9244c2afb
Author: Breck Yunits <breck7@gmail.com>
Date: Wed Dec 4 07:08:25 2024 -1000
diff --git a/components/CodeEditor.js b/components/CodeEditor.js
index 0c95fa7..5048112 100644
--- a/components/CodeEditor.js
+++ b/components/CodeEditor.js
@@ -35,18 +35,9 @@ class CodeEditorComponent extends AbstractParticleComponentParser {
rehighlight() {
if (this._parser === this.root.parser) return
- console.log("rehighlighting")
+ console.log("rehighlighting needed")
this._parser = this.root.parser
-
- const editor = this.codeMirrorInstance
- const originalContent = editor.getValue()
- const cursorPosition = editor.getCursor()
- editor.setValue("//\n" + originalContent)
- // Restore the original content
- setTimeout(() => {
- editor.setValue(originalContent)
- editor.setCursor(cursorPosition) // Restore the cursor position
- }, 0) // Use a timeout to ensure rendering happens
+ // todo: figure this out. codemirror seems to not want to repaint.
}
codeWidgets = []
diff --git a/dist/app.js b/dist/app.js
index 7839169..a1c9d35 100644
--- a/dist/app.js
+++ b/dist/app.js
@@ -51,14 +51,10 @@ class CodeEditorComponent extends AbstractParticleComponentParser {
this._parser = this.root.parser
const editor = this.codeMirrorInstance
- const originalContent = editor.getValue()
- const cursorPosition = editor.getCursor()
- editor.setValue("//\n" + originalContent)
- // Restore the original content
setTimeout(() => {
- editor.setValue(originalContent)
- editor.setCursor(cursorPosition) // Restore the cursor position
- }, 0) // Use a timeout to ensure rendering happens
+ const content = editor.getValue()
+ editor.setValue(content)
+ }, 1000) // Use a timeout to ensure rendering happens
}
codeWidgets = []
------------------------------------------------------------
commit a2e1f926c6a0610c66e9c12e28de6ec52a38452f
Author: Breck Yunits <breck7@gmail.com>
Date: Wed Dec 4 06:52:27 2024 -1000
diff --git a/dist/libs.js b/dist/libs.js
index 85b8baa..20880d7 100644
--- a/dist/libs.js
+++ b/dist/libs.js
@@ -17726,7 +17726,7 @@ Particle.iris = `sepal_length,sepal_width,petal_length,petal_width,species
4.9,2.5,4.5,1.7,virginica
5.1,3.5,1.4,0.2,setosa
5,3.4,1.5,0.2,setosa`
-Particle.getVersion = () => "99.1.0"
+Particle.getVersion = () => "99.2.0"
class AbstractExtendibleParticle extends Particle {
_getFromExtended(cuePath) {
const hit = this._getParticleFromExtended(cuePath)
@@ -21689,30 +21689,45 @@ const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm
const importRegex = /^(import |[a-zA-Z\_\-\.0-9\/]+\.(scroll|parsers)$|https?:\/\/.+\.(scroll|parsers)$)/gm
const importOnlyRegex = /^importOnly/
const isUrl = path => urlRegex.test(path)
-// URL content cache
+// URL content cache with pending requests tracking
const urlCache = {}
+const pendingRequests = {}
async function fetchWithCache(url) {
const now = Date.now()
const cached = urlCache[url]
if (cached) return cached
- try {
- const response = await fetch(url)
- if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
- const content = await response.text()
- urlCache[url] = {
- content,
- timestamp: now,
- exists: true
- }
- } catch (error) {
- console.error(`Error fetching ${url}:`, error)
- urlCache[url] = {
- content: "",
- timestamp: now,
- exists: false
- }
- }
- return urlCache[url]
+ // If there's already a pending request for this URL, return that promise
+ if (pendingRequests[url]) {
+ return pendingRequests[url]
+ }
+ // Create new request and store in pending
+ const requestPromise = (async () => {
+ try {
+ const response = await fetch(url)
+ if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`)
+ const content = await response.text()
+ const result = {
+ content,
+ timestamp: now,
+ exists: true
+ }
+ urlCache[url] = result
+ return result
+ } catch (error) {
+ console.error(`Error fetching ${url}:`, error)
+ const result = {
+ content: "",
+ timestamp: now,
+ exists: false
+ }
+ urlCache[url] = result
+ return result
+ } finally {
+ delete pendingRequests[url]
+ }
+ })()
+ pendingRequests[url] = requestPromise
+ return requestPromise
}
class DiskWriter {
constructor() {
diff --git a/package.json b/package.json
index bece262..0940f33 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
"dependencies": {
"jquery": "^3.6.0",
"scroll-cli": "^161.1.0",
- "scrollsdk": "^99.0.0"
+ "scrollsdk": "^99.2.0"
},
"devDependencies": {
"tap": "^18.7.2"
------------------------------------------------------------
commit d8e05c62b72bf46cadca04a43ef0bfe427a66f60
Author: Breck Yunits <breck7@gmail.com>
Date: Wed Dec 4 06:21:39 2024 -1000
diff --git a/dist/constants.js b/dist/constants.js
index e61c128..d7ef43a 100644
--- a/dist/constants.js
+++ b/dist/constants.js
@@ -1 +1 @@
-const AppConstants = {"parsers":"columnNameAtom\n extends stringAtom\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n extends stringAtom\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint comment\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n extends anyAtom\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\nscriptAnyAtom\n extends codeAtom\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom keywordAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint string\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\njavascriptAtom\n extends stringAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\"<a \") ? false : true\n return shouldLinkify ? this.replaceNotes(Utils.linkify(originalText)) : originalText\n }\n replaceNotes(originalText) {\n // Skip the replacements if there are no footnotes or the text has none.\n if (!this.root.footnotes.length || !originalText.includes(\"^\")) return originalText\n this.root.footnotes.forEach((note, index) => {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `<a href=\"#${note.htmlId}\" class=\"scrollNoteLink\" id=\"${linkBack}\"><sup>${note.label}</sup></a>`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.text}${this.closingTag}`\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return `</${tag}>`\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `<span class=\"scrollBlink\">${super.buildHtml()}</span>\n <script>setInterval(()=>{ Array.from(document.getElementsByClassName(\"scrollBlink\")).forEach(el => el.style.visibility = el.style.visibility === \"hidden\" ? \"visible\" : \"hidden\") }, 500)</script>`\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"linkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"</center>\")\n return `<center>${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser\n javascript\n compileSubparticles() {\n return this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildHtml() {\n return super.buildHtml() + this.compileSubparticles()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `<div style=\"text-indent:${(this.getIndentLevel() - 1) * 20}px;\"><input type=\"checkbox\" ${this.checked} id=\"${this.id}\"><label for=\"${this.id}\">` + super.text + `</label></div>`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `</${listType}>` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return `<style>\\n.${this.constructor.name} li::marker {content: \"${this.cue} \";}\\n</style>`\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* <span id=\"counter${id}\" title=\"0\">0</span><script>setInterval(()=>{ const el = document.getElementById('counter${id}'); el.title = parseFloat(el.title) + ${increment}; el.textContent = Math.floor(parseFloat(el.title)).toLocaleString()}, 100)</script> ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"</details>\")\n return `<details>${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `<a class=\"scrollFootNoteUsageLink\" href=\"#noteUsage${this.noteDefinitionIndex}\">${this.label}</a> ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"</div>\")\n return `<div class=\"scrollSection\">` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}></${this.tag}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return `<audio controls ${this.getAsHtmlAttributes(\"loop autoplay\".split(\" \"))}><source src=\"${this.filename}\" type=\"audio/mpeg\"></audio>`\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom\n heightParser\n cueFromId\n atoms cueAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\n widthParser\n // todo: fix inheritance bug\n cueFromId\n atoms cueAtom\n heightParser\n cueFromId\n atoms cueAtom\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* <span class=\"scrollStopwatchParser\" id=\"stopwatch${id}\">0.0</span><script>{let startTime = parseFloat(new URLSearchParams(window.location.search).get('start') || 0); document.getElementById('stopwatch${id}').title = startTime; setInterval(()=>{ const el = document.getElementById('stopwatch${id}'); el.title = parseFloat(el.title) + .1; el.textContent = (parseFloat(el.title)).toFixed(1)}, 100)}</script> `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"</div>\") // Single columns are self-closing after section break.\n return stackContents + `<div class=\"scrollColumns\" style=\"column-width:${columnWidth}ch;column-count:${maxColumns};max-width:${maxTotalWidth}ch;\">`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"</div>\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"</div>\")\n return `<style>.scrollContainerParser{width: 100%; box-sizing: border-box; max-width: ${this.maxWidth}; margin: 0 auto;}</style>` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `<div class=\"${this.defaultClass}\"><span>${this.dinkus}</span></div>`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n javascript\n buildHtml() {\n return `<hr>`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `<style>.abstractIconButtonParser {position:absolute;top:0.25rem; }.abstractIconButtonParser svg {fill: rgba(204,204,204,.8);width:1.875rem;height:1.875rem; padding: 0 7px;} .abstractIconButtonParser:hover svg{fill: #333;}</style><a href=\"${this.link}\" class=\"doNotPrint abstractIconButtonParser\" style=\"${this.style}\">${this.svg}</a>`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg <svg fill=\"#000000\" xmlns=\"http://www.w3.org/2000/svg\" width=\"800px\" height=\"800px\" viewBox=\"0 0 52 52\" enable-background=\"new 0 0 52 52\" xml:space=\"preserve\"><path d=\"M38.6,20.4c-1-6.5-6.7-11.5-13.5-11.5c-7.6,0-13.7,6.1-13.7,13.7c0,0.3,0,0.7,0.1,1c-5,0.4-8.9,4.6-8.9,9.6 c0,5.4,4.3,9.7,9.7,9.7h11.5c-0.8-0.8-8.1-8.1-8.1-8.1c-0.4-0.4-0.4-0.9,0-1.3l1.3-1.3c0.4-0.4,0.9-0.4,1.3,0l3.5,3.5 c0.4,0.4,1.1,0.1,1.1-0.4V21.8c0-0.4,0.5-0.9,1-0.9h1.9c0.5,0,0.9,0.4,0.9,0.9v13.4c0,0.6,0.8,0.8,1.1,0.4l3.5-3.5 c0.4-0.4,0.9-0.4,1.3,0l1.3,1.3c0.4,0.4,0.4,0.9,0,1.3L26,42.9h12.3v0c6.1-0.1,11-5.1,11-11.3C49.4,25.5,44.6,20.6,38.6,20.4z\"/></svg><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->\n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg <svg width=\"800px\" height=\"800px\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M21.1213 2.70705C19.9497 1.53548 18.0503 1.53547 16.8787 2.70705L15.1989 4.38685L7.29289 12.2928C7.16473 12.421 7.07382 12.5816 7.02986 12.7574L6.02986 16.7574C5.94466 17.0982 6.04451 17.4587 6.29289 17.707C6.54127 17.9554 6.90176 18.0553 7.24254 17.9701L11.2425 16.9701C11.4184 16.9261 11.5789 16.8352 11.7071 16.707L19.5556 8.85857L21.2929 7.12126C22.4645 5.94969 22.4645 4.05019 21.2929 2.87862L21.1213 2.70705ZM18.2929 4.12126C18.6834 3.73074 19.3166 3.73074 19.7071 4.12126L19.8787 4.29283C20.2692 4.68336 20.2692 5.31653 19.8787 5.70705L18.8622 6.72357L17.3068 5.10738L18.2929 4.12126ZM15.8923 6.52185L17.4477 8.13804L10.4888 15.097L8.37437 15.6256L8.90296 13.5112L15.8923 6.52185ZM4 7.99994C4 7.44766 4.44772 6.99994 5 6.99994H10C10.5523 6.99994 11 6.55223 11 5.99994C11 5.44766 10.5523 4.99994 10 4.99994H5C3.34315 4.99994 2 6.34309 2 7.99994V18.9999C2 20.6568 3.34315 21.9999 5 21.9999H16C17.6569 21.9999 19 20.6568 19 18.9999V13.9999C19 13.4477 18.5523 12.9999 18 12.9999C17.4477 12.9999 17 13.4477 17 13.9999V18.9999C17 19.5522 16.5523 19.9999 16 19.9999H5C4.44772 19.9999 4 19.5522 4 18.9999V7.99994Z\"/></svg>\n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg <svg viewBox=\"3 5 24 20\" width=\"24\" height=\"20\" xmlns=\"http://www.w3.org/2000/svg\"><g transform=\"matrix(1, 0, 0, 1, 0, -289.0625)\"><path style=\"opacity:1;stroke:none;stroke-width:0.49999997;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1\" d=\"M 5 5 C 4.2955948 5 3.6803238 5.3628126 3.3242188 5.9101562 L 14.292969 16.878906 C 14.696939 17.282876 15.303061 17.282876 15.707031 16.878906 L 26.675781 5.9101562 C 26.319676 5.3628126 25.704405 5 25 5 L 5 5 z M 3 8.4140625 L 3 23 C 3 24.108 3.892 25 5 25 L 25 25 C 26.108 25 27 24.108 27 23 L 27 8.4140625 L 17.121094 18.292969 C 15.958108 19.455959 14.041892 19.455959 12.878906 18.292969 L 3 8.4140625 z \" transform=\"translate(0,289.0625)\" id=\"rect4592\"/></g></svg>\n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg <svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12.7166 3.79541C12.2835 3.49716 11.7165 3.49716 11.2834 3.79541L4.14336 8.7121C3.81027 8.94146 3.60747 9.31108 3.59247 9.70797C3.54064 11.0799 3.4857 13.4824 3.63658 15.1877C3.7504 16.4742 4.05336 18.1747 4.29944 19.4256C4.41371 20.0066 4.91937 20.4284 5.52037 20.4284H8.84433C8.98594 20.4284 9.10074 20.3111 9.10074 20.1665V15.9754C9.10074 14.9627 9.90433 14.1417 10.8956 14.1417H13.4091C14.4004 14.1417 15.204 14.9627 15.204 15.9754V20.1665C15.204 20.3111 15.3188 20.4284 15.4604 20.4284H18.4796C19.0806 20.4284 19.5863 20.0066 19.7006 19.4256C19.9466 18.1747 20.2496 16.4742 20.3634 15.1877C20.5143 13.4824 20.4594 11.0799 20.4075 9.70797C20.3925 9.31108 20.1897 8.94146 19.8566 8.7121L12.7166 3.79541ZM10.4235 2.49217C11.3764 1.83602 12.6236 1.83602 13.5765 2.49217L20.7165 7.40886C21.4457 7.91098 21.9104 8.73651 21.9448 9.64736C21.9966 11.0178 22.0564 13.5119 21.8956 15.3292C21.7738 16.7067 21.4561 18.4786 21.2089 19.7353C20.9461 21.0711 19.7924 22.0001 18.4796 22.0001H15.4604C14.4691 22.0001 13.6655 21.1791 13.6655 20.1665V15.9754C13.6655 15.8307 13.5507 15.7134 13.4091 15.7134H10.8956C10.754 15.7134 10.6392 15.8307 10.6392 15.9754V20.1665C10.6392 21.1791 9.83561 22.0001 8.84433 22.0001H5.52037C4.20761 22.0001 3.05389 21.0711 2.79113 19.7353C2.54392 18.4786 2.22624 16.7067 2.10437 15.3292C1.94358 13.5119 2.00338 11.0178 2.05515 9.64736C2.08957 8.73652 2.55427 7.91098 3.28346 7.40886L10.4235 2.49217Z\"/></svg>\n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg <svg xmlns=\"http://www.w3.org/2000/svg\" direction=\"ltr\" width=\"231.52568231268793\" height=\"249.33156169975996\" viewBox=\"297.27169239534896 1273.121785992385 231.52568231268793 249.33156169975996\" stroke-linecap=\"round\" stroke-linejoin=\"round\" data-color-mode=\"dark\" class=\"tl-container tl-theme__force-sRGB tl-theme__dark\" ><defs/><g transform=\"matrix(1, 0, 0, 1, 395.9682, 1413.3618)\" opacity=\"1\"><g transform=\"scale(1)\"><path d=\"M-5.7342,-1.1484 T-5.0182,-4.6536 -2.6672,-12.9768 1.7374,-22.3903 8.4597,-30.0892 16.5455,-35.2796 25.3478,-37.8247 34.4641,-37.2199 43.0676,-33.4606 50.27,-27.1118 55.7861,-19.7972 59.8042,-11.6764 61.6844,-2.3157 60.8049,7.9615 57.3259,17.5954 51.2605,25.5515 41.9451,31.9237 30.6489,36.2084 18.9812,37.6038 8.4915,36.637 -1.3063,34.5174 -10.6869,31.6604 -19.0463,27.0252 -26.0612,20.1562 -31.864,11.1271 -35.2772,1.0066 -35.7963,-9.1933 -35.203,-19.2337 -32.52,-28.3259 -27.3388,-37.4245 -20.2857,-45.7085 -11.787,-53.0395 -2.4314,-58.4864 7.1724,-61.8739 17.5002,-65.6025 28.2859,-68.1034 38.514,-69.1081 48.1844,-69.9134 57.9136,-70.1787 67.4056,-69.5971 76.297,-67.7446 84.5233,-63.97 92.0158,-57.5003 98.1976,-48.5614 102.8839,-38.056 105.9936,-27.6097 107.074,-18.168 107.175,-8.2882 106.6452,1.9768 105.4734,11.7698 104.0279,21.1013 101.7439,31.0445 98.0977,40.6386 92.7822,49.1039 86.2847,57.2337 78.6099,64.7113 69.8339,70.974 61.2863,75.092 52.4283,78.2186 42.5338,80.7945 32.7733,82.6629 22.8568,83.6574 12.2803,83.3538 2.4971,81.4828 -6.3367,78.789 -15.1061,75.8011 -24.259,72.3779 -33.2286,68.991 -41.5707,64.8363 -49.153,59.1909 -55.8375,52.2938 -62.9311,43.9553 -68.4604,34.4882 -71.2761,23.5855 -72.748,12.7452 -73.7673,2.0453 -74.1187,-8.7386 -72.293,-19.3856 -68.7376,-30.4053 -64.12,-39.7852 -59.1825,-47.8931 -53.7695,-56.4512 -48.1998,-65.7676 -43.1389,-74.401 -36.9006,-81.0179 -28.4317,-86.2639 -18.3342,-90.3521 -8.9259,-93.9355 0.1927,-98.7179 9.3551,-103.3537 18.7651,-106.7552 29.7482,-110.3584 40.9287,-113.446 50.87,-115.2231 57.8279,-116.0074 60.4065,-116.0632 A7.8326,7.8326 0 0 1 61.1735,-100.4168 T58.6018,-100.2201 51.1634,-99.3008 41.695,-96.884 32.4044,-94.0187 22.65,-91.382 13.4974,-87.3756 5.0519,-83.1026 -5.0443,-78.6175 -15.7236,-74.4743 -24.6193,-70.2681 -31.274,-62.5903 -36.3827,-53.9362 -41.2833,-46.2905 -46.0679,-38.5161 -50.639,-30.6576 -54.6876,-22.4063 -57.6038,-12.3346 -58.5104,-1.8678 -57.6022,9.4969 -56.0628,20.477 -53.6434,29.0487 -48.8908,36.7517 -42.4075,43.9784 -35.3064,50.6236 -27.3553,55.2062 -17.6884,59.4064 -7.6442,63.6638 2.4427,67.0449 12.7308,69.4843 22.9294,70.2171 33.2547,69.3921 42.4972,67.7384 52.2753,64.8425 61.8649,60.9142 69.5292,56.0828 76.3604,49.5927 82.431,42.3194 87.219,34.619 90.6076,25.5217 92.547,16.0352 93.8703,7.1315 94.4972,-2.5268 94.3672,-13.3681 93.3204,-24.4252 90.5377,-34.1656 85.7321,-43.6441 78.7334,-51.8057 68.7542,-56.0559 57.9857,-57.2713 48.079,-57.0198 38.4535,-56.3204 28.5246,-55.0036 18.6104,-52.4796 9.6402,-49.559 0.7193,-46.2981 -7.1531,-41.3715 -13.708,-34.8957 -19.5435,-26.853 -22.8356,-17.0622 -23.363,-6.5446 -21.9969,3.1439 -17.1086,11.9113 -9.276,18.744 -0.0685,22.6058 9.3636,24.796 19.2422,25.8817 29.052,25.0411 37.9338,21.3502 45.6819,13.8498 49.6675,4.5228 49.2802,-5.1854 45.3277,-14.4455 38.8603,-21.7879 30.5488,-25.7753 21.3356,-24.3975 13.6687,-19.2391 8.8217,-10.9919 6.423,-2.3609 5.7342,1.1484 A5.8481,5.8481 0 0 1 -5.7342,-1.1484 Z\" stroke-linecap=\"round\"/></g></g></svg>\n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return `<div class=\"abstractTextLinkParser\"><a href=\"${this.link}\">${this.text}</a></div>`\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n <script>\n sendFormViaEmail = form => {\n const mailto = new URL(\"mailto:\")\n const params = []\n const { value, title } = form.querySelector('button[type=\"submit\"]')\n params.push(`subject=${encodeURIComponent(value)}`)\n params.push(`to=${encodeURIComponent(title)}`)\n const oneTextarea = form.querySelector('textarea[title=\"oneTextarea\"]')\n const body = oneTextarea ? codeMirrorInstance.getValue() : Array.from(new FormData(form)).map(([name, value]) => `${name} ${value}`).join(\"\\\\n\")\n params.push(`body=${encodeURIComponent(body)}`)\n mailto.search = params.join(\"&\")\n window.open(mailto.href, '_blank')\n }\n </script>\n string style\n <style> .scrollFormParser {\n font-family: \"Gill Sans\", \"Bitstream Vera Sans\", sans-serif;\n }\n .scrollFormParser input , .scrollFormParser textarea{\n padding: 10px;\n margin-bottom: 10px;\n width: 100%;\n box-sizing: border-box;\n } .scrollFormParser label {\n display: block;\n margin-bottom: 5px;\n }\n </style>\n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = `<textarea placeholder=\"${placeholder}\" id=\"${Name}\" name=\"${Name}\" ${IsRequired ? \"required\" : \"\"}></textarea>`\n else\n tag = `<input placeholder=\"${placeholder}\" type=\"${type}\" id=\"${Name}\" name=\"${Name}\" ${IsRequired ? \"required\" : \"\"}>`\n return `<div><label for=\"${Name}\" title=\"${IsRequired ? \"Required\" : \"\"}\">${ucFirst}${IsRequired ? \"*\" : \"\"}:</label>${tag}</div>`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}<form ${isEmail ? \"onsubmit='sendFormViaEmail(this); return false;'\" : ` method='post' action='${formDestination}'`} class=\"scrollFormParser\">${this.inputs}<button value=\"${subject}\" title=\"${formDestination}\" class=\"scrollButton\" type=\"submit\">${callToAction}</button>${this.footer}</form>`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal codeMirror.css scrollLibs.js constants.js\n string requireOnce\n <link rel=\"stylesheet\" href=\"codeMirror.css\">\n <script src=\"scrollLibs.js\"></script>\n <script src=\"constants.js\"></script>\n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `<textarea title=\"oneTextarea\" rows=\"${Math.min(this.root.measures.length * 2, 30)}\" placeholder=\"${this.placeholder}\" id=\"${Name}\" name=\"${Name}\"></textarea>\n <script id=\"${Name}Parsers\" type=\"text/plain\">${this.parsersBundle}</script>\n <script>{\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n let {width, height} = document.getElementById('${Name}').getBoundingClientRect();\n const sp = new Particle(AppConstants.parsers)\n sp.filter(particle => particle.getLine().match(parserRegex)).forEach(part => {\n part.appendLine(\"boolean suggestInAutocomplete false\")\n })\n const customScrollParser = new HandParsersProgram(sp.toString() + document.getElementById(\"${Name}Parsers\").textContent).compileAndReturnRootParser()\n codeMirrorInstance = new ParsersCodeMirrorMode(\"custom\", () => customScrollParser, undefined, CodeMirror).register().fromTextAreaWithAutocomplete(document.getElementById(\"${Name}\"), {\n lineWrapping: false,\n lineNumbers: false\n })\n codeMirrorInstance.setSize(width, height);\n codeMirrorInstance.setValue(\\`${this.value}\\`); }</script>`\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `<a class=\"scrollContinueReadingLink\" href=\"${linkRelativeToCompileTarget}\">Continue reading...</a>`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `<div class=\"scrollSnippetContainer\">${this.makeSnippet(file.file.scrollProgram, buildSettings)}</div>`\n }).join(\"\\n\\n\")\n return `<div class=\"scrollColumns\" style=\"column-width:35ch;\">${snippets}</div>`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue nav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return `<nav class=\"scrollNavParser\">` + this.root.getFilesByTags(this.content).map(file => {\n const { linkTitle, permalink } = file.file.scrollProgram\n return `<a href=\"${permalink}\">${linkTitle}</a>` \n }).join(this.get(\"join\") || \" \") + `</nav>`\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `<div><a href=\"${permalink}\">${title}</a><div>${description}...</div><div class=\"subdued\" style=\"text-align:right;\">${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}</div></div>`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `<div id=\"${id}\" class=\"scrollNoticesParser\" style=\"display:none;\"></div><script>(function(){\n const params = new URLSearchParams(window.location.search)\n if (!params.size) return\n document.getElementById('${id}').innerHTML = Array.from(params.entries()).map(([key, value]) => {\n if (key === \"error\") \n return '<div style=\"color:red\">Error: ' + value + '</div>'\n if (key === \"success\")\n return '<div style=\"color:green\">Success: ' + value + '</div>'\n return '<div>' + key + ': ' + value + '</div>'\n }).join(\"\") || \"No query parameters found\"\n document.getElementById('${id}').style.display = \"block\"\n })()</script>`\n }\nprintSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.buildTxt().replace(/\\</g, \"<\")}</code>`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual} = this\n const expected = this.subparticlesToString()\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType anyAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n javascript\n buildHtml() {\n return `<div class=\"printDateParser\">${this.day}</div>`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n async _buildFileType(extension) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash, permalink } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const outputFiles = this.content?.split(\" \") || [\"\"]\n for (let name of outputFiles) {\n const link = name || permalink.replace(\".html\", \".\" + extension.toLowerCase())\n try {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileTo(capitalized))\n root.log(`💾 Built ${link} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne() { await this._buildFileType(this.extension) }\nbuildCsvParser\n popularity 0.000096\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo() {\n await this._buildFileType(this.extension)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(externalFilesCopied) {\n await this._copyExternalFiles(externalFilesCopied)\n await super.buildTwo()\n }\n async _copyExternalFiles(externalFilesCopied = {}) {\n if (!this.isNodeJs()) return\n const {root} = this\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Write RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Write concepts to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom anyAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildCommandParser\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Write measures to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom anyAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo() {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${this.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the <html lang=\"\"> tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Meta tag description.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType anyAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType anyAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `<div style=\"text-align: ${index % 2 ? \"right\" : \"left\"};\" class=\"scrollChat ${index % 2 ? \"scrollChatRight\" : \"scrollChatLeft\"}\"><span>${line.asString}</span></div>` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = typeof d3 === \"undefined\" ? require('d3') : d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = typeof d3 === \"undefined\" ? require('d3') : d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/</g, \"<\")\n return {\n title, titleLink: relativePath + permalink, text, date, wordCount, minutes\n }\n }\n columnNames = \"title titleLink text date wordCount minutes\".split(\" \")\nscrollPostsParser\n popularity 0.000024\n cue posts\n description Posts as table.\n extends abstractPostsParser\n example\n // Print a search table:\n posts\n printTable\n tableSearch\n // Dump a CSV of blog:\n buildHtml\n buildCsv\n buildJson\nscrollPostsMetaParser\n popularity 0.000024\n cue postsMeta\n description Post meta as table.\n extends abstractPostsParser\n javascript\n columnNames = [\"date\", \"year\", \"title\", \"permalink\", \"authors\", \"tags\", \"wordCount\", \"minutes\"]\n postToRow(file) {\n const {date, year, title, permalink, authors, tags, wordCount, minutes} = file.file.scrollProgram\n return {\n date, year, title, permalink, authors, tags, wordCount, minutes\n }\n }\nprintFeedParser\n popularity 0.000048\n description Print group to RSS.\n extends abstractPostsParser\n example\n printFeed index\n printFeed cars/index\n buildRss\n javascript\n buildRss() {\n const {dayjs} = this.root\n const scrollPrograms = this.files.map(file => file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>\n <rss version=\"2.0\">\n <channel>\n <title>${title}</title>\n <link>${baseUrl}</link>\n <description>${description}</description>\n <lastBuildDate>${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}</lastBuildDate>\n <language>en-us</language>\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n </channel>\n </rss>`\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.code.replace(/\\</g, \"<\")}</code>`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `<div class=\"codeWithHeader\"><div class=\"codeHeader\">${this.content}</div>${super.buildHtml()}</div>`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n <script>\n document.addEventListener(\"DOMContentLoaded\", () => document.querySelectorAll(\".scrollCodeBlock\").forEach(block =>\n {\n if (!navigator.clipboard) return\n const button = document.createElement(\"span\")\n button.classList.add(\"scrollCopyButton\")\n block.appendChild(button)\n button.addEventListener(\"click\", async () => {\n await navigator.clipboard.writeText(block.innerText)\n button.classList.add(\"scrollCopiedButton\")\n })\n }\n ))\n </script>\nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = `<style>\n .heatrixContainer {\n margin: auto;\n }.heatrixRow {white-space: nowrap;}\n ${cssId} .heatrixAtom {\n font-family: arial;\n border-radius: 2px;\n border: 1px solid transparent;\n display: inline-block;\n margin: 1px;\n text-align: center;\n vertical-align: middle;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n .heatrixAtom a {\n color: black;\n }\n ${cssId} .heatrixAtom{\n width: ${defaultWidth};\n height: ${defaultHeight};\n font-size: ${fontSize};\n line-height: ${lineHeight};\n }\n </style>`\n const firstRow = this.table[0]\n return (\n `<div class=\"heatrixContainer\" id=\"heatrix${this.uid}\">${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `<div class=\"heatrixRow heatrixRow${rowIndex}\">${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return `<div class=\"heatrixAtom heatrixColumn${columnIndex}${valueClass}\" style=\"${inlineStyle}\"><a title=\"${label}\" ${href}>${label}</a></div>`\n })\n .join(\"\")}</div>`\n })\n .join(\"\\n\") +\n \"</div>\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom anyAtom\n cueFromId\n single\n colorParser\n atoms cueAtom anyAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType anyAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal leaflet.css leaflet.js scrollLibs.js\n string requireOnce\n <link rel=\"stylesheet\" href=\"leaflet.css\">\n <script src=\"leaflet.js\"></script>\n <script src=\"scrollLibs.js\"></script>\n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `<div id=\"${mapId}\" style=\"${style}\"></div>\n <script>\n {\n if (!window.maps) window.maps = {}\n const moveToMyLocation = () => {\n if (!navigator.geolocation) return\n navigator.geolocation.getCurrentPosition((position) => {\n const { latitude, longitude } = position.coords\n window.maps.${mapId}.setView([latitude, longitude])\n L.circleMarker([latitude, longitude], {\n fillOpacity: 0.8,\n radius: 20,\n weight: 4\n })\n .addTo(window.maps.${mapId})\n }, () => {})\n }\n const lat = ${this.get(\"lat\") ?? 37.8}\n const long = ${this.get(\"long\") ?? 4}\n if (${this.has(\"geolocate\")}){\n moveToMyLocation()\n }\n const zoomLevel = ${this.get(\"zoom\") ?? 4}\n const hover = '${this.get(\"hover\") || \"<b>{title}</b><br>{description}\"}'\n const template = ${JSON.stringify(template)}\n const points = ${JSON.stringify((this.parent.coreTable || []).filter(point => point.lat && point.long), undefined, 2)}\n window.maps.${mapId} = L.map(\"map${id}\").setView([lat, long], zoomLevel)\n const map = window.maps.${mapId}\n const tileOptions = {\n \"default\": {\n baseLayer: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',\n attribution: '<a href=\"https://www.openstreetmap.org/\">OpenStreetMap</a> contributors'\n },\n light: {\n baseLayer: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',\n attribution: '<a href=\"https://www.openstreetmap.org/\">OpenStreetMap</a> contributors <a href=\"https://carto.com/ attributions\">CARTO</a>'\n },\n }\n const {baseLayer, attribution} = tileOptions.${this.get(\"tiles\") || \"default\"}\n L.tileLayer(baseLayer, {\n attribution,\n maxZoom: 19\n }).addTo(map);\n points.forEach(point => {\n L.circleMarker([point.lat, point.long], {...template, ...Object.fromEntries(\n Object.entries(point).filter(([key, value]) => value !== null)\n )})\n .addTo(map)\n .bindPopup(new Particle(point).evalTemplateString(hover))\n })\n }\n </script>`\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal d3.js plot.js\n string requireOnce\n <script src=\"d3.js\"></script>\n <script src=\"plot.js\"></script>\n example\n plot\n inScope abstractColumnNameParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `<div id=\"${id}\"></div><script>\n {\n let loadChart = async () => {\n const data = ${this.dataCode}\n const get = (col, index ) => col !== \"undefined\" ? col : (index === undefined ? undefined : Object.keys(data[0])[index])\n document.querySelector(\"#${id}\").append(Plot.plot(${this.plotOptions}))\n }\n loadChart()\n }\n </script>`\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n }`\n }\nscatterplotParser\n extends abstractPlotParser\n description Scatterplot Widget.\n // todo: make copyFromExternal work with inheritance\n string copyFromExternal d3.js plot.js\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true,\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal sparkline.js\n string requireOnce <script src=\"sparkline.js\"></script>\n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return `<span id=\"${id}\"></span><script>new Sparkline(document.getElementById(\"${id}\"), {dotRadius: 0, width: ${width}, height: ${height}, lineColor: \"${lineColor}\", tooltip: (value,index) => ${start} + index + \": \" + value}).draw(${JSON.stringify(columnValues)})</script>`\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `<th>${column.name}</th>\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `<a href=\"${link}\">${text}</a>`\n else if (col.name.endsWith(\"Url\")) tagged = `<a href=\"${content}\">${col.name.replace(\"Url\", \"\")}</a>`\n else if (isUrl) tagged = `<a href=\"${content}\">${text}</a>`\n str += `<td>${tagged}</td>\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `<tr>${this.toRow(row)}</tr>`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `<table id=\"table${this._getUid()}\" class=\"scrollTable\">\n <thead><tr>${this.tableHeader.join(\"\\n\")}</tr></thead>\n <tbody>${this.tableBody}</tbody>\n </table>`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal katex.min.css katex.min.js\n string requireOnce\n <link rel=\"stylesheet\" href=\"katex.min.css\">\n <script defer src=\"katex.min.js\"></script>\n <script>\n document.addEventListener(\"DOMContentLoaded\", () => document.querySelectorAll(\".scrollKatex\").forEach(el =>\n {\n katex.render(el.innerText, el, {\n throwOnError: false\n });\n }\n ))\n </script>\n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `<div class=\"scrollKatex\" id=\"${id}\">${content + this.subparticlesToString()}</div>`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `<style>#helpfulNotFound{margin: 100px 0;}</style><h1 id=\"helpfulNotFound\"></h1><script defer src=\"/helpfulNotFound.js\"></script><script>document.addEventListener(\"DOMContentLoaded\", () => new NotFoundApp('${this.content}'))</script>`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal jquery-3.7.1.min.js slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `<style>html {font-size: var(--scrollBaseFontSize, 28px);} body {margin: auto; width: 500px;}.slideshowNav{text-align: center; margin-bottom:20px; font-size: 24px;color: rgba(204,204,204,.8);} a{text-decoration: none; color: rgba(204,204,204,.8);}</style><script defer src=\"jquery-3.7.1.min.js\"></script><div class=\"slideshowNav\"></div><script defer src=\"slideshow.js\"></script>`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal jquery-3.7.1.min.js datatables.css dayjs.min.js datatables.js tableSearch.js\n string requireOnce\n <script defer src=\"jquery-3.7.1.min.js\"></script>\n <style>.dt-search{font-family: \"SF Pro\", \"Helvetica Neue\", \"Segoe UI\", \"Arial\";}</style>\n <link rel=\"stylesheet\" href=\"datatables.css\">\n <script defer src=\"datatables.js\"></script>\n <script defer src=\"dayjs.min.js\"></script>\n <script defer src=\"tableSearch.js\"></script>\n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>${this.css}</style>`\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCs() {\n return this.css\n }\ninlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractScrollParser\n cueFromId\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return `<style>/* ${this.content} */\\n${this.css}</style>`\n }\n get css() {\n return this.atoms.slice(1).map(filename => this.root.readFile(filename)).join(\"\\n\\n\")\n }\n buildCs() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>html, body { background: ${this.content};}</style>`\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>html, body { color: ${this.content};}</style>`\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return `<style>html, body, h1,h2,h3 { font-family: ${font};}</style>`\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return `<link rel=\"stylesheet\" type=\"text/css\" href=\"${this.filename}\">`\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return `<script src=\"${this.filename}\"></script>`\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `<tr>` + items.map(particle => `<td>${particle.cue}<span>${particle.content}</span></td>`).join(\"\\n\") + `</tr>\\n`\n }\n buildHtml() {\n return `<table class=\"scrollDashboard\">${this.tableBody}</table>`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = this[method]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = next[method]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.code.replace(/\\</g, \"<\")}</code>`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType anyAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\ninspectBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal inspector.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `<div class=\"inspectorParticle\"><span class=\"inspectorParticleId\">${particle.constructor.name}</span>${particle.atoms.map((atom, index) => `<span class=\"inspectorAtom\">${atom}<span class=\"inspectorAtomType\">${atomTypes[index]}</span></span>`).join(\" \")}${(particle.length ? `<br><div class=\"inspectorSubparticles\">` + particle.map(mapFn).join(\"<br>\") + `</div>` : \"\")}</div>`}\n return this.selectedParticles.map(mapFn).join(\"<br>\")\n }\n buildHtml() {\n return `<link rel=\"stylesheet\" href=\"inspector.css\">` + this.code\n }\ninspectAboveParser\n description Inspect particle above.\n extends inspectBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return `<style>${this.css}</style>`\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCs() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(`</${tag}>`)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `<br>`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => `<iframe src=\"${url}\" frameborder=\"0\"></iframe>`).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `<figcaption>${caption.buildHtml()}</figcaption>` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `<figure class=\"scrollCaptionedFigure\" style=\"${widthStyle + float}\">${this.getFigureContent(buildSettings)}${captionFig}</figure>`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser linkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"linkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return `<a href=\"${clickLink}\" target=\"${target}\" ${className} ${id}><img src=\"${linkRelativeToCompileTarget}\" ${dimensionAttributes}loading=\"lazy\"></a>`\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \"qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `<div class=\"scrollYouTubeHolder\"><iframe class=\"scrollYouTubeEmbed\" src=\"${url}\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe></div>`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\ninlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractScrollParser\n cueFromId\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return `<script>/* ${this.content} */\\n${this.contents}</script>`\n }\n get contents() {\n return this.atoms.slice(1).map(filename => this.root.readFile(filename)).join(\";\\n\\n\")\n }\n buildJs() {\n return this.contents\n }\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType scriptAnyAtom\n javascript\n buildHtml() {\n return `<script>${this.scriptContent}</script>`\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return `<script>window.${varName} = ${this.root.readFile(this.filename)}</script>`\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<style>${style}</style><a class=\"keyboardNav doNotPrint\" style=\"left:.5rem;\" href=\"${linkToPrevious}\"><</a><a class=\"keyboardNav doNotPrint\" style=\"right:.5rem;\" href=\"${linkToNext}\">></a>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = `<script>document.addEventListener('keydown', function(event) {\n if (document.activeElement !== document.body) return\n if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return // dont interfere with keyboard back button shortcut\n const getLinks = () => document.getElementsByClassName(\"scrollKeyboardNav\")[0].getElementsByTagName(\"a\")\n if (event.key === \"ArrowLeft\")\n getLinks()[0].click()\n else if (event.key === \"ArrowRight\")\n getLinks()[1].click()\n });</script>`\n return `<div class=\"scrollKeyboardNav\" style=\"display:none;\"><a href=\"${linkToPrevious}\">${linkToPrevious}</a> · ${root.permalink} · <a href=\"${linkToNext}\">${linkToNext}</a>${script}</div>`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"<h></h>\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `<style>${css}</style><div id=\"scrollLeetSheet\">` + doc.map(obj => `<div class=\"${obj.category}\"><a href=\"${this.makeLink(obj.examples, obj.id)}\">${obj.isPopular ? \"<b>\" : \"\"}${obj.id}</a> ${obj.description}${obj.isPopular ? \"</b>\" : \"\"}${obj.note}</div>`).join(\"\\n\") + \"</div>\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n buildCsv() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category} = obj\n return {\n id,\n isPopular,\n description,\n popularity,\n category\n }\n })\n return new Particle(this.root.lodash.sortBy(rows, \"isPopular\")).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"<p><b>Parser Definition Parsers</b> define parsers that acquire, analyze and act on code.</p>\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"<p><b>Atom Definition Parsers</b> analyze the atoms in a line.</p>\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` <span class=\"note\">A${category.replace(\"Phase\", \"\").substr(1)}Time.</span>`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `<link rel=\"icon\" href=\"${favicon}\">` : \"\"\n const rssTag = rssFeedUrl ? `<link rel=\"alternate\" type=\"application/rss+xml\" title=\"${title}\" href=\"${rssFeedUrl}\">` : \"\"\n const gitTag = gitRepo ? `<link rel=\"source\" type=\"application/git\" title=\"Source Code Repository\" href=\"${gitRepo}\">` : \"\"\n return `<head>\n <meta charset=\"utf-8\">\n <title>${title}</title>\n <script>/* This HTML was generated by 📜 Scroll v${scrollVersion}. https://scroll.pub */</script>\n <style>@media print {.doNotPrint {display: none !important;}}</style>\n <link rel=\"canonical\" href=\"${canonicalUrl}\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <meta name=\"description\" content=\"${description}\">\n <meta name=\"generator\" content=\"Scroll v${scrollVersion}\">\n <meta property=\"og:title\" content=\"${title}\">\n <meta property=\"og:description\" content=\"${description}\">\n <meta property=\"og:image\" content=\"${openGraphImage}\">\n ${faviconTag}\n ${gitTag}\n ${rssTag}\n <meta name=\"twitter:card\" content=\"summary_large_image\">\n </head>\n <body>`\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `<blockquote class=\"scrollQuote\">${this.subparticlesToString()}</blockquote>`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return `<meta http-equiv=\"Refresh\" content=\"0; url='${this.getAtom(1)}'\" />`\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.root.makeFullPath(this.content))\n }\n buildHtml() {\n return `<pre>${this.buildTxt()}</pre>`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n <script src=\"scripts/nested/hello.js\"></script>\n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `${name}.css`)\n }\n buildHtml() {\n return this.files.map(name => `<link rel=\"stylesheet\" type=\"text/css\" href=\"${name}\">`).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType anyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `</${this.closeTag}>` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nlinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an <a> tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cueFromId\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get attributes() {\n const attrs = [`href=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends linkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends linkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends linkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `<span class=\"scrollDateline\">${day} — </span>` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: `</${tag}>`, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom anyAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nchatLineParser\n popularity 0.009887\n catchAllAtomType anyAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(reduceParticle => {\n return {\n source: reduceParticle.getAtom(1),\n reduction: reduceParticle.getAtom(2),\n name: reduceParticle.getAtom(3) || reduceParticle.getAtomsFrom(1).join(\"_\")\n }\n })\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n if (reduction === \"concat\") {\n newRow[col.name] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[col.name] = group[0][sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[col.name] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom atomAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.getAtom(1)\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || untypedScalarValue === undefined) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return atom === \"\" || atom === undefined\n else if (operator === \"notEmpty\") return !(atom === \"\" || atom === undefined)\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n return this.getAtomsFrom(1)\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType stringAtom\n cue compose\n example\n table\n compose My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n catchAllAtomType stringAtom\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`<a href=\"${value.includes(\"@\") ? \"mailto:\" : \"\"}${value}\">${name}</a>`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.getAtom(1)\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom atomAtom\n catchAllAtomType atomAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType anyAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nscrollFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType scriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType anyAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nscrollFillParser\n cue fill\n extends abstractColumnNameParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nquoteLineParser\n popularity 0.004172\n catchAllAtomType anyAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return localStorage.getItem(fileOrUrl) || \"\"\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = localStorage.getItem(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n localStorage.setItem(url, await blob.text())\n return localStorage.getItem(url)\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `<a href=\"${this.editUrl}\" class=\"abstractTextLinkParser\">Edit</a>`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"161.0.3\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particle[methodName])\n .map((particle, index) => particle[methodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"<body>\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `<!DOCTYPE html>\\n<html lang=\"${this.lang}\">\\n${bodyTag}${content}\\n</body>\\n</html>` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll() {\n await this.load()\n await this.buildOne()\n await this.buildTwo()\n }\n async buildOne() {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne()\n }\n }\n async buildTwo(externalFilesCopied = {}) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(externalFilesCopied)\n }\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concept[measure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) row[measureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else row[measureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCache[measureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCache[measureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCache[measureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|footer$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"footer\")) {\n const pushes = particle.getParticles(\"footer\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` <item>\n <title>${title}</title>\n <link>${canonicalUrl}</link>\n <pubDate>${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}</pubDate>\n </item>`\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType anyAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n description Table from inline delimited data.\n catchAllAtomType anyAtom\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
\ No newline at end of file
+const AppConstants = {"parsers":"columnNameAtom\n extends stringAtom\npercentAtom\n paint constant.numeric.float\n extends stringAtom\n // todo: this currently extends from stringAtom b/c scrollsdk needs to be fixed. seems like if extending from number then the hard coded number typescript regex takes precedence over a custom regex\ncountAtom\n extends integerAtom\nyearAtom\n extends integerAtom\npreBuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant.character.escape\ndelimiterAtom\n description String to use as a delimiter.\n paint string\nbulletPointAtom\n description Any token used as a bullet point such as \"-\" or \"1.\" or \">\"\n paint keyword\ncomparisonAtom\n enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith\n paint constant\npersonNameAtom\n extends stringAtom\nurlAtom\n paint constant.language\nabsoluteUrlAtom\n paint constant.language\n regex (ftp|https?)://.+\nemailAddressAtom\n extends stringAtom\npermalinkAtom\n paint string\n description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.\nfilePathAtom\n extends stringAtom\ntagOrUrlAtom\n description An HTML tag or a url.\n paint constant.language\nhtmlAttributesAtom\n paint comment\nhtmlTagAtom\n paint constant.language\n enum div span p a img ul ol li h1 h2 h3 h4 h5 h6 header nav section article aside main footer input button form label select option textarea table tr td th tbody thead tfoot br hr meta link script style title code\nclassNameAtom\n paint constant\nhtmlIdAtom\n extends anyAtom\nfontFamilyAtom\n enum Arial Helvetica Verdana Georgia Impact Tahoma Slim\n paint constant\nbuildCommandAtom\n extends cueAtom\n description Give build command atoms their own color.\n paint constant\ncssAnyAtom\n extends codeAtom\ncssLengthAtom\n extends codeAtom\nhtmlAnyAtom\n extends codeAtom\ninlineMarkupNameAtom\n description Options to turn on some inline markups.\n enum bold italics code katex none\nscriptAnyAtom\n extends codeAtom\ntileOptionAtom\n enum default light\nmeasureNameAtom\n extends cueAtom\n // A regex for column names for max compatibility with a broad range of data science tools:\n regex [a-zA-Z][a-zA-Z0-9]*\nabstractConstantAtom\n paint entity.name.tag\njavascriptSafeAlphaNumericIdentifierAtom\n regex [a-zA-Z0-9_]+\n reservedAtoms enum extends function static if while export return class for default require var let const new\nanyAtom\nbaseParsersAtom\n description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.\n // todo Remove?\n enum blobParser errorParser\n paint variable.parameter\nenumAtom\n paint constant.language\nbooleanAtom\n enum true false\n extends enumAtom\natomParserAtom\n enum prefix postfix omnifix\n paint constant.numeric\natomPropertyNameAtom\n paint variable.parameter\natomTypeIdAtom\n examples integerAtom keywordAtom someCustomAtom\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes atomTypeIdAtom\n paint storage\nconstantIdentifierAtom\n examples someId myVar\n // todo Extend javascriptSafeAlphaNumericIdentifier\n regex [a-zA-Z]\\w+\n paint constant.other\n description A atom that can be assigned to the parser in the target language.\nconstructorFilePathAtom\nenumOptionAtom\n // todo Add an enumOption top level type, so we can add data to an enum option such as a description.\n paint string\natomExampleAtom\n description Holds an example for a atom with a wide range of options.\n paint string\nextraAtom\n paint invalid\nfileExtensionAtom\n examples js txt doc exe\n regex [a-zA-Z0-9]+\n paint string\nnumberAtom\n paint constant.numeric\nfloatAtom\n extends numberAtom\n regex \\-?[0-9]*\\.?[0-9]*\n paint constant.numeric.float\nintegerAtom\n regex \\-?[0-9]+\n extends numberAtom\n paint constant.numeric.integer\ncueAtom\n description A atom that indicates a certain parser to use.\n paint keyword\njavascriptCodeAtom\nlowercaseAtom\n regex [a-z]+\nparserIdAtom\n examples commentParser addParser\n description This doubles as the class name in Javascript. If this begins with `abstract`, then the parser will be considered an abstract parser, which cannot be used by itself but provides common functionality to parsers that extend it.\n paint variable.parameter\n extends javascriptSafeAlphaNumericIdentifierAtom\n enumFromAtomTypes parserIdAtom\ncueAtom\n paint constant.language\nregexAtom\n paint string.regexp\nreservedAtomAtom\n description A atom that a atom cannot contain.\n paint string\npaintTypeAtom\n enum comment comment.block comment.block.documentation comment.line constant constant.character.escape constant.language constant.numeric constant.numeric.complex constant.numeric.complex.imaginary constant.numeric.complex.real constant.numeric.float constant.numeric.float.binary constant.numeric.float.decimal constant.numeric.float.hexadecimal constant.numeric.float.octal constant.numeric.float.other constant.numeric.integer constant.numeric.integer.binary constant.numeric.integer.decimal constant.numeric.integer.hexadecimal constant.numeric.integer.octal constant.numeric.integer.other constant.other constant.other.placeholder entity entity.name entity.name.class entity.name.class.forward-decl entity.name.constant entity.name.enum entity.name.function entity.name.function.constructor entity.name.function.destructor entity.name.impl entity.name.interface entity.name.label entity.name.namespace entity.name.section entity.name.struct entity.name.tag entity.name.trait entity.name.type entity.name.union entity.other.attribute-name entity.other.inherited-class invalid invalid.deprecated invalid.illegal keyword keyword.control keyword.control.conditional keyword.control.import keyword.declaration keyword.operator keyword.operator.arithmetic keyword.operator.assignment keyword.operator.bitwise keyword.operator.logical keyword.operator.atom keyword.other markup markup.bold markup.deleted markup.heading markup.inserted markup.italic markup.list.numbered markup.list.unnumbered markup.other markup.quote markup.raw.block markup.raw.inline markup.underline markup.underline.link meta meta.annotation meta.annotation.identifier meta.annotation.parameters meta.block meta.braces meta.brackets meta.class meta.enum meta.function meta.function-call meta.function.parameters meta.function.return-type meta.generic meta.group meta.impl meta.interface meta.interpolation meta.namespace meta.paragraph meta.parens meta.path meta.preprocessor meta.string meta.struct meta.tag meta.toc-list meta.trait meta.type meta.union punctuation punctuation.accessor punctuation.definition.annotation punctuation.definition.comment punctuation.definition.generic.begin punctuation.definition.generic.end punctuation.definition.keyword punctuation.definition.string.begin punctuation.definition.string.end punctuation.definition.variable punctuation.section.block.begin punctuation.section.block.end punctuation.section.braces.begin punctuation.section.braces.end punctuation.section.brackets.begin punctuation.section.brackets.end punctuation.section.group.begin punctuation.section.group.end punctuation.section.interpolation.begin punctuation.section.interpolation.end punctuation.section.parens.begin punctuation.section.parens.end punctuation.separator punctuation.separator.continuation punctuation.terminator source source.language-suffix.embedded storage storage.modifier storage.type storage.type keyword.declaration.type storage.type.class keyword.declaration.class storage.type.enum keyword.declaration.enum storage.type.function keyword.declaration.function storage.type.impl keyword.declaration.impl storage.type.interface keyword.declaration.interface storage.type.struct keyword.declaration.struct storage.type.trait keyword.declaration.trait storage.type.union keyword.declaration.union string string.quoted.double string.quoted.other string.quoted.single string.quoted.triple string.regexp string.unquoted support support.class support.constant support.function support.module support.type text text.html text.xml variable variable.annotation variable.function variable.language variable.other variable.other.constant variable.other.member variable.other.readwrite variable.parameter\n paint string\nscriptUrlAtom\nsemanticVersionAtom\n examples 1.0.0 2.2.1\n regex [0-9]+\\.[0-9]+\\.[0-9]+\n paint constant.numeric\ndateAtom\n paint string\nstringAtom\n paint string\natomAtom\n paint string\n description A non-empty single atom string.\n regex .+\nexampleAnyAtom\n examples lorem ipsem\n // todo Eventually we want to be able to parse correctly the examples.\n paint comment\n extends stringAtom\nblankAtom\ncommentAtom\n paint comment\ncodeAtom\n paint comment\njavascriptAtom\n extends stringAtom\nmetaCommandAtom\n extends cueAtom\n description Give meta command atoms their own color.\n paint constant.numeric\n // Obviously this is not numeric. But I like the green color for now.\n We need a better design to replace this \"paint\" concept\n https://github.com/breck7/scrollsdk/issues/186\ntagAtom\n extends permalinkAtom\ntagWithOptionalFolderAtom\n description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.\n extends stringAtom\nscrollThemeAtom\n enum roboto gazette dark tufte prestige\n paint constant\nabstractScrollParser\n atoms cueAtom\n javascript\n buildHtmlSnippet(buildSettings) {\n return this.buildHtml(buildSettings)\n }\n buildTxt() {\n return \"\"\n }\n getHtmlRequirements(buildSettings) {\n const {requireOnce} = this\n if (!requireOnce)\n return \"\"\n const set = buildSettings?.alreadyRequired || this.root.alreadyRequired\n if (set.has(requireOnce))\n return \"\"\n set.add(requireOnce)\n return requireOnce + \"\\n\\n\"\n }\nabstractAftertextParser\n description Text followed by markup commands.\n extends abstractScrollParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser\n javascript\n get markupInserts() {\n const { originalTextPostLinkify } = this\n return this.filter(particle => particle.isMarkup)\n .map(particle => particle.getInserts(originalTextPostLinkify))\n .filter(i => i)\n .flat()\n }\n get originalText() {\n return this.content ?? \"\"\n }\n get originalTextPostLinkify() {\n const { originalText } = this\n const shouldLinkify = this.get(\"linkify\") === \"false\" || originalText.includes(\"<a \") ? false : true\n return shouldLinkify ? this.replaceNotes(Utils.linkify(originalText)) : originalText\n }\n replaceNotes(originalText) {\n // Skip the replacements if there are no footnotes or the text has none.\n if (!this.root.footnotes.length || !originalText.includes(\"^\")) return originalText\n this.root.footnotes.forEach((note, index) => {\n const needle = note.cue\n const {linkBack} = note\n if (originalText.includes(needle)) originalText = originalText.replace(new RegExp(\"\\\\\" + needle + \"\\\\b\"), `<a href=\"#${note.htmlId}\" class=\"scrollNoteLink\" id=\"${linkBack}\"><sup>${note.label}</sup></a>`)\n })\n return originalText\n }\n get text() {\n const { originalTextPostLinkify, markupInserts } = this\n let adjustment = 0\n let newText = originalTextPostLinkify\n markupInserts.sort((a, b) => {\n if (a.index !== b.index)\n return a.index - b.index\n // If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.\n if (b.index === b.endIndex) // unless the endIndex is the same as index\n return a.endIndex - b.endIndex\n return b.endIndex - a.endIndex\n })\n markupInserts.forEach(insertion => {\n insertion.index += adjustment\n const consumeStartCharacters = insertion.consumeStartCharacters ?? 0\n const consumeEndCharacters = insertion.consumeEndCharacters ?? 0\n newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)\n adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters\n })\n return newText\n }\n tag = \"p\"\n get className() {\n if (this.get(\"classes\"))\n return this.get(\"classes\")\n const classLine = this.getParticle(\"class\")\n if (classLine && classLine.applyToParentElement) return classLine.content\n return this.defaultClassName\n }\n defaultClassName = \"scrollParagraph\"\n get isHidden() {\n return this.has(\"hidden\")\n }\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n this.buildSettings = buildSettings\n const { className, styles } = this\n const classAttr = className ? `class=\"${this.className}\"` : \"\"\n const tag = this.get(\"tag\") || this.tag\n if (tag === \"none\") // Allow no tag for aftertext in tables\n return this.text\n const id = this.has(\"id\") ? \"\" : `id=\"${this.htmlId}\" ` // always add an html id\n return this.getHtmlRequirements(buildSettings) + `<${tag} ${id}${this.htmlAttributes}${classAttr}${styles}>${this.text}${this.closingTag}`\n }\n get closingTag() {\n const tag = this.get(\"tag\") || this.tag\n return `</${tag}>`\n }\n get htmlAttributes() {\n const attrs = this.filter(particle => particle.isAttribute)\n return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(\" \") + \" \" : \"\"\n }\n get styles() {\n const style = this.getParticle(\"style\")\n const fontFamily = this.getParticle(\"font\")\n const color = this.getParticle(\"color\")\n if (!style && !fontFamily && !color)\n return \"\"\n return ` style=\"${style?.content};${fontFamily?.css};${color?.css}\"`\n }\n get htmlId() {\n return this.get(\"id\") || \"particle\" + this.index\n }\nscrollParagraphParser\n // todo Perhaps rewrite this from scratch and move out of aftertext.\n extends abstractAftertextParser\n catchAllAtomType stringAtom\n description A paragraph.\n boolean suggestInAutocomplete false\n cueFromId\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n // Hacky, I know.\n const newLine = this.has(\"inlineMarkupsOn\") ? undefined : this.appendLine(\"inlineMarkupsOn\")\n const compiled = super.buildHtml(buildSettings)\n if (newLine)\n newLine.destroy()\n return compiled\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n const dateline = this.getParticle(\"dateline\")\n return (dateline ? dateline.day + \"\\n\\n\" : \"\") + (this.originalText || \"\") + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nauthorsParser\n popularity 0.007379\n // multiple authors delimited by \" and \"\n boolean isPopular true\n extends scrollParagraphParser\n description Set author(s) name(s).\n example\n authors Breck Yunits\n https://breckyunits.com Breck Yunits\n // note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtmlForPrint() {\n // hacky. todo: cleanup\n const originalContent = this.content\n this.setContent(`by ${originalContent}`)\n const html = super.buildHtml()\n this.setContent(originalContent)\n return html\n }\n buildTxtForPrint() {\n return 'by ' + super.buildTxt()\n }\n buildHtml() {\n return \"\"\n }\n buildTxt() {\n return \"\"\n }\n defaultClassName = \"printAuthorsParser\"\nblinkParser\n description Just for fun.\n extends scrollParagraphParser\n example\n blink Carpe diem!\n cue blink\n javascript\n buildHtml() {\n return `<span class=\"scrollBlink\">${super.buildHtml()}</span>\n <script>setInterval(()=>{ Array.from(document.getElementsByClassName(\"scrollBlink\")).forEach(el => el.style.visibility = el.style.visibility === \"hidden\" ? \"visible\" : \"hidden\") }, 500)</script>`\n }\nscrollButtonParser\n extends scrollParagraphParser\n cue button\n description A button.\n postParser\n description Post a particle.\n example\n button Click me\n javascript\n defaultClassName = \"scrollButton\"\n tag = \"button\"\n get htmlAttributes() {\n const link = this.getFromParser(\"linkParser\")\n const post = this.getParticle(\"post\")\n if (post) {\n const method = \"post\"\n const action = link?.link || \"\"\n const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()\n return ` onclick=\"fetch('${action}', {method: '${method}', body: '${formData}', headers: {'Content-Type': 'application/x-www-form-urlencoded'}}).then(async (message) => {const el = document.createElement('div'); el.textContent = await message.text(); this.insertAdjacentElement('afterend', el);}); return false;\" `\n }\n return super.htmlAttributes + (link ? `onclick=\"window.location='${link.link}'\"` : \"\")\n }\n getFromParser(parserId) {\n return this.find(particle => particle.doesExtend(parserId))\n }\ncatchAllParagraphParser\n popularity 0.115562\n description A paragraph.\n extends scrollParagraphParser\n boolean suggestInAutocomplete false\n boolean isPopular true\n boolean isArticleContent true\n atoms stringAtom\n javascript\n getErrors() {\n const errors = super.getErrors() || []\n return this.parent.has(\"testStrict\") ? errors.concat(this.makeError(`catchAllParagraphParser should not have any matches when testing with testStrict.`)) : errors\n }\n get originalText() {\n return this.getLine() || \"\"\n }\nscrollCenterParser\n popularity 0.006415\n cue center\n description A centered section.\n extends scrollParagraphParser\n example\n center\n This paragraph is centered.\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"</center>\")\n return `<center>${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\nabstractIndentableParagraphParser\n extends scrollParagraphParser\n inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser\n javascript\n compileSubparticles() {\n return this.map(particle => particle.buildHtml())\n .join(\"\\n\")\n .trim()\n }\n buildHtml() {\n return super.buildHtml() + this.compileSubparticles()\n }\n buildTxt() {\n return this.getAtom(0) + \" \" + super.buildTxt()\n }\nchecklistTodoParser\n popularity 0.000193\n extends abstractIndentableParagraphParser\n example\n [] Get milk\n description A task todo.\n cue []\n string checked \n javascript\n get text() {\n return `<div style=\"text-indent:${(this.getIndentLevel() - 1) * 20}px;\"><input type=\"checkbox\" ${this.checked} id=\"${this.id}\"><label for=\"${this.id}\">` + super.text + `</label></div>`\n }\n get id() {\n return this.get(\"id\") || \"item\" + this._getUid()\n }\nchecklistDoneParser\n popularity 0.000072\n extends checklistTodoParser\n description A completed task.\n string checked checked\n cue [x]\n example\n [x] get milk\nlistAftertextParser\n popularity 0.014325\n extends abstractIndentableParagraphParser\n example\n - I had a _new_ thought.\n description A list item.\n cue -\n javascript\n defaultClassName = \"\"\n buildHtml() {\n const {index, parent} = this\n const particleClass = this.constructor\n const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)\n const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)\n const { listType } = this\n return (isStartOfList ? `<${listType} ${this.attributes}>` : \"\") + `${super.buildHtml()}` + (isEndOfList ? `</${listType}>` : \"\")\n }\n get attributes() {\n return \"\"\n }\n tag = \"li\"\n listType = \"ul\"\nabstractCustomListItemParser\n extends listAftertextParser\n javascript\n get requireOnce() {\n return `<style>\\n.${this.constructor.name} li::marker {content: \"${this.cue} \";}\\n</style>`\n }\n get attributes() {\n return `class=\"${this.constructor.name}\"`\n }\norderedListAftertextParser\n popularity 0.004485\n extends listAftertextParser\n description A list item.\n example\n 1. Hello world\n pattern ^\\d+\\. \n javascript\n listType = \"ol\"\n get attributes() { return ` start=\"${this.getAtom(0)}\"`}\nquickQuoteParser\n popularity 0.000482\n cue >\n example\n > The only thing we have to fear is fear itself. - FDR\n boolean isPopular true\n extends abstractIndentableParagraphParser\n description A quote.\n javascript\n defaultClassName = \"scrollQuote\"\n tag = \"blockquote\"\nscrollCounterParser\n description Visualize the speed of something.\n extends scrollParagraphParser\n cue counter\n example\n counter 4.5 Babies Born\n atoms cueAtom numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const atoms = line.split(\" \")\n atoms.shift() // drop the counter atom\n const perSecond = parseFloat(atoms.shift()) // get number\n const increment = perSecond/10\n const id = this._getUid()\n this.setLine(`* <span id=\"counter${id}\" title=\"0\">0</span><script>setInterval(()=>{ const el = document.getElementById('counter${id}'); el.title = parseFloat(el.title) + ${increment}; el.textContent = Math.floor(parseFloat(el.title)).toLocaleString()}, 100)</script> ` + atoms.join(\" \"))\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nexpanderParser\n popularity 0.000072\n cueFromId\n description An collapsible HTML details tag.\n extends scrollParagraphParser\n example\n expander Knock Knock\n Who's there?\n javascript\n buildHtml() {\n this.parent.sectionStack.push(\"</details>\")\n return `<details>${super.buildHtml()}`\n }\n buildTxt() {\n return this.content\n }\n tag = \"summary\"\n defaultClassName = \"\"\nfootnoteDefinitionParser\n popularity 0.001953\n description A footnote. Can also be used as section notes.\n extends scrollParagraphParser\n boolean isFootnote true\n pattern ^\\^.+$\n // We need to quickLinks back in scope because there is currently a bug in ScrollSDK/parsers where if a parser extending a parent class has a child parser defined, then any regex parsers in the parent class will not be tested unless explicitly included in scope again.\n inScope quickLinkParser\n labelParser\n description If you want to show a custom label for a footnote. Default label is the note definition index.\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n get htmlId() {\n return `note${this.noteDefinitionIndex}`\n }\n get label() {\n // In the future we could allow common practices like author name\n return this.get(\"label\") || `[${this.noteDefinitionIndex}]`\n }\n get linkBack() {\n return `noteUsage${this.noteDefinitionIndex}`\n }\n get text() {\n return `<a class=\"scrollFootNoteUsageLink\" href=\"#noteUsage${this.noteDefinitionIndex}\">${this.label}</a> ${super.text}`\n }\n get noteDefinitionIndex() {\n return this.parent.footnotes.indexOf(this) + 1\n }\n buildTxt() {\n return this.getAtom(0) + \": \" + super.buildTxt()\n }\nabstractHeaderParser\n extends scrollParagraphParser\n example\n # Hello world\n javascript\n buildHtml(buildSettings) {\n if (this.isHidden) return \"\"\n if (this.parent.sectionStack)\n this.parent.sectionStack.push(\"</div>\")\n return `<div class=\"scrollSection\">` + super.buildHtml(buildSettings)\n }\n buildTxt() {\n const line = super.buildTxt()\n return line + \"\\n\" + \"=\".repeat(line.length)\n }\n isHeader = true\nh1Parser\n popularity 0.017918\n description An html h1 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue #\n boolean isPopular true\n javascript\n tag = \"h1\"\nh2Parser\n popularity 0.005257\n description An html h2 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ##\n boolean isPopular true\n javascript\n tag = \"h2\"\nh3Parser\n popularity 0.001085\n description An html h3 tag.\n extends abstractHeaderParser\n boolean isArticleContent true\n cue ###\n javascript\n tag = \"h3\"\nh4Parser\n popularity 0.000289\n description An html h4 tag.\n extends abstractHeaderParser\n cue ####\n javascript\n tag = \"h4\"\nscrollQuestionParser\n popularity 0.004244\n description A question.\n extends h4Parser\n cue ?\n example\n ? Why is the sky blue?\n javascript\n defaultClassName = \"scrollQuestion\"\nh5Parser\n description An html h5 tag.\n extends abstractHeaderParser\n cue #####\n javascript\n tag = \"h5\"\nprintTitleParser\n popularity 0.007572\n description Print title.\n extends abstractHeaderParser\n boolean isPopular true\n example\n title Eureka\n printTitle\n cueFromId\n javascript\n buildHtml(buildSettings) {\n // Hacky, I know.\n const {content} = this\n if (content === undefined)\n this.setContent(this.root.title)\n const { permalink } = this.root\n if (!permalink) {\n this.setContent(content) // Restore it as it was.\n return super.buildHtml(buildSettings)\n }\n const newLine = this.appendLine(`link ${permalink}`)\n const compiled = super.buildHtml(buildSettings)\n newLine.destroy()\n this.setContent(content) // Restore it as it was.\n return compiled\n }\n get originalText() {\n return this.content ?? this.root.title ?? \"\"\n }\n defaultClassName = \"printTitleParser\"\n tag = \"h1\"\ncaptionAftertextParser\n popularity 0.003207\n description An image caption.\n cue caption\n extends scrollParagraphParser\n boolean isPopular true\nabstractMediaParser\n extends scrollParagraphParser\n inScope scrollMediaLoopParser scrollAutoplayParser\n int atomIndex 1\n javascript\n buildTxt() {\n return \"\"\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n getAsHtmlAttribute(attr) {\n if (!this.has(attr)) return \"\"\n const value = this.get(attr)\n return value ? `${attr}=\"${value}\"` : attr\n }\n getAsHtmlAttributes(list) {\n return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(\" \")\n }\n buildHtml() {\n return `<${this.tag} src=\"${this.filename}\" controls ${this.getAsHtmlAttributes(\"width height loop autoplay\".split(\" \"))}></${this.tag}>`\n }\nscrollMusicParser\n popularity 0.000024\n extends abstractMediaParser\n cue music\n description Play sound files.\n example\n music sipOfCoffee.m4a\n javascript\n buildHtml() {\n return `<audio controls ${this.getAsHtmlAttributes(\"loop autoplay\".split(\" \"))}><source src=\"${this.filename}\" type=\"audio/mpeg\"></audio>`\n }\nquickSoundParser\n popularity 0.000024\n extends scrollMusicParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)\n int atomIndex 0\nscrollVideoParser\n popularity 0.000024\n extends abstractMediaParser\n cue video\n example\n video spirit.mp4\n description Play video files.\n widthParser\n cueFromId\n atoms cueAtom\n heightParser\n cueFromId\n atoms cueAtom\n javascript\n tag = \"video\"\nquickVideoParser\n popularity 0.000024\n extends scrollVideoParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(mp4|webm|avi|mov)\n int atomIndex 0\n widthParser\n // todo: fix inheritance bug\n cueFromId\n atoms cueAtom\n heightParser\n cueFromId\n atoms cueAtom\nquickParagraphParser\n popularity 0.001881\n cue *\n extends scrollParagraphParser\n description A paragraph.\n boolean isArticleContent true\n example\n * I had a _new_ idea.\nscrollStopwatchParser\n description A stopwatch.\n extends scrollParagraphParser\n cue stopwatch\n example\n stopwatch\n atoms cueAtom\n catchAllAtomType numberAtom\n javascript\n buildHtml() {\n const line = this.getLine()\n const id = this._getUid()\n this.setLine(`* <span class=\"scrollStopwatchParser\" id=\"stopwatch${id}\">0.0</span><script>{let startTime = parseFloat(new URLSearchParams(window.location.search).get('start') || 0); document.getElementById('stopwatch${id}').title = startTime; setInterval(()=>{ const el = document.getElementById('stopwatch${id}'); el.title = parseFloat(el.title) + .1; el.textContent = (parseFloat(el.title)).toFixed(1)}, 100)}</script> `)\n const html = super.buildHtml()\n this.setLine(line)\n return html\n }\nthinColumnsParser\n popularity 0.003690\n extends abstractAftertextParser\n cueFromId\n catchAllAtomType integerAtom\n description Thin columns.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n columnWidth = 35\n columnGap = 20\n buildHtml() {\n const {columnWidth, columnGap, maxColumns} = this\n const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap\n const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.\n if (this.singleColumn) this.parent.sectionStack.push(\"</div>\") // Single columns are self-closing after section break.\n return stackContents + `<div class=\"scrollColumns\" style=\"column-width:${columnWidth}ch;column-count:${maxColumns};max-width:${maxTotalWidth}ch;\">`\n }\n get maxColumns() {\n return this.singleColumn ? 1 : parseInt(this.getAtom(1) ?? 10)\n }\nwideColumnsParser\n popularity 0.000386\n extends thinColumnsParser\n description Wide columns.\n javascript\n columnWidth = 90\nwideColumnParser\n popularity 0.003376\n extends wideColumnsParser\n description A wide column section.\n boolean singleColumn true\nmediumColumnsParser\n popularity 0.003376\n extends thinColumnsParser\n description Medium width columns.\n javascript\n columnWidth = 65\nmediumColumnParser\n popularity 0.003376\n extends mediumColumnsParser\n description A medium column section.\n boolean singleColumn true\nthinColumnParser\n popularity 0.003376\n extends thinColumnsParser\n description A thin column section.\n boolean singleColumn true\nendColumnsParser\n popularity 0.007789\n extends abstractAftertextParser\n cueFromId\n description End columns.\n javascript\n buildHtml() {\n return \"</div>\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\nscrollContainerParser\n popularity 0.000096\n cue container\n description A centered HTML div.\n catchAllAtomType cssLengthAtom\n extends abstractAftertextParser\n boolean isHtml true\n javascript\n get maxWidth() {\n return this.atoms[1] || \"1200px\"\n }\n buildHtmlSnippet() {\n return \"\"\n }\n tag = \"div\"\n defaultClassName = \"scrollContainerParser\"\n buildHtml() {\n this.parent.bodyStack.push(\"</div>\")\n return `<style>.scrollContainerParser{width: 100%; box-sizing: border-box; max-width: ${this.maxWidth}; margin: 0 auto;}</style>` + super.buildHtml()\n }\n get text() { return \"\"}\n get closingTag() { return \"\"}\nabstractDinkusParser\n extends abstractAftertextParser\n boolean isDinkus true\n javascript\n buildHtml() {\n return `<div class=\"${this.defaultClass}\"><span>${this.dinkus}</span></div>`\n }\n defaultClass = \"abstractDinkusParser\"\n buildTxt() {\n return this.dinkus\n }\n get dinkus() {\n return this.content || this.getLine()\n }\nhorizontalRuleParser\n popularity 0.000362\n cue ---\n description A horizontal rule.\n extends abstractDinkusParser\n javascript\n buildHtml() {\n return `<hr>`\n }\nscrollDinkusParser\n popularity 0.010828\n cue ***\n description A dinkus. Breaks section.\n boolean isPopular true\n extends abstractDinkusParser\n javascript\n dinkus = \"*\"\ncustomDinkusParser\n cue dinkus\n description A custom dinkus.\n extends abstractDinkusParser\nendOfPostDinkusParser\n popularity 0.005740\n extends abstractDinkusParser\n description End of post dinkus.\n boolean isPopular true\n cue ****\n javascript\n dinkus = \"⁂\"\nabstractIconButtonParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return `<style>.abstractIconButtonParser {position:absolute;top:0.25rem; }.abstractIconButtonParser svg {fill: rgba(204,204,204,.8);width:1.875rem;height:1.875rem; padding: 0 7px;} .abstractIconButtonParser:hover svg{fill: #333;}</style><a href=\"${this.link}\" class=\"doNotPrint abstractIconButtonParser\" style=\"${this.style}\">${this.svg}</a>`\n }\ndownloadButtonParser\n popularity 0.006294\n description Link to download/WWS page.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style position:relative;\n string svg <svg fill=\"#000000\" xmlns=\"http://www.w3.org/2000/svg\" width=\"800px\" height=\"800px\" viewBox=\"0 0 52 52\" enable-background=\"new 0 0 52 52\" xml:space=\"preserve\"><path d=\"M38.6,20.4c-1-6.5-6.7-11.5-13.5-11.5c-7.6,0-13.7,6.1-13.7,13.7c0,0.3,0,0.7,0.1,1c-5,0.4-8.9,4.6-8.9,9.6 c0,5.4,4.3,9.7,9.7,9.7h11.5c-0.8-0.8-8.1-8.1-8.1-8.1c-0.4-0.4-0.4-0.9,0-1.3l1.3-1.3c0.4-0.4,0.9-0.4,1.3,0l3.5,3.5 c0.4,0.4,1.1,0.1,1.1-0.4V21.8c0-0.4,0.5-0.9,1-0.9h1.9c0.5,0,0.9,0.4,0.9,0.9v13.4c0,0.6,0.8,0.8,1.1,0.4l3.5-3.5 c0.4-0.4,0.9-0.4,1.3,0l1.3,1.3c0.4,0.4,0.4,0.9,0,1.3L26,42.9h12.3v0c6.1-0.1,11-5.1,11-11.3C49.4,25.5,44.6,20.6,38.6,20.4z\"/></svg><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->\n javascript\n get link() {\n return this.content\n }\neditButtonParser\n popularity 0.013963\n description Print badge top right.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n // SVG from https://github.com/32pixelsCo/zest-icons\n string svg <svg width=\"800px\" height=\"800px\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M21.1213 2.70705C19.9497 1.53548 18.0503 1.53547 16.8787 2.70705L15.1989 4.38685L7.29289 12.2928C7.16473 12.421 7.07382 12.5816 7.02986 12.7574L6.02986 16.7574C5.94466 17.0982 6.04451 17.4587 6.29289 17.707C6.54127 17.9554 6.90176 18.0553 7.24254 17.9701L11.2425 16.9701C11.4184 16.9261 11.5789 16.8352 11.7071 16.707L19.5556 8.85857L21.2929 7.12126C22.4645 5.94969 22.4645 4.05019 21.2929 2.87862L21.1213 2.70705ZM18.2929 4.12126C18.6834 3.73074 19.3166 3.73074 19.7071 4.12126L19.8787 4.29283C20.2692 4.68336 20.2692 5.31653 19.8787 5.70705L18.8622 6.72357L17.3068 5.10738L18.2929 4.12126ZM15.8923 6.52185L17.4477 8.13804L10.4888 15.097L8.37437 15.6256L8.90296 13.5112L15.8923 6.52185ZM4 7.99994C4 7.44766 4.44772 6.99994 5 6.99994H10C10.5523 6.99994 11 6.55223 11 5.99994C11 5.44766 10.5523 4.99994 10 4.99994H5C3.34315 4.99994 2 6.34309 2 7.99994V18.9999C2 20.6568 3.34315 21.9999 5 21.9999H16C17.6569 21.9999 19 20.6568 19 18.9999V13.9999C19 13.4477 18.5523 12.9999 18 12.9999C17.4477 12.9999 17 13.4477 17 13.9999V18.9999C17 19.5522 16.5523 19.9999 16 19.9999H5C4.44772 19.9999 4 19.5522 4 18.9999V7.99994Z\"/></svg>\n javascript\n get link() {\n return this.content || this.root.editUrl || \"\"\n }\n get style() {\n return this.parent.findParticles(\"editButton\")[0] === this ? \"right:2rem;\": \"position:relative;\"\n }\nemailButtonParser\n popularity 0.006294\n description Email button.\n extends abstractIconButtonParser\n catchAllAtomType emailAddressAtom\n // todo: should just be \"optionalAtomType\"\n string style position:relative;\n string svg <svg viewBox=\"3 5 24 20\" width=\"24\" height=\"20\" xmlns=\"http://www.w3.org/2000/svg\"><g transform=\"matrix(1, 0, 0, 1, 0, -289.0625)\"><path style=\"opacity:1;stroke:none;stroke-width:0.49999997;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1\" d=\"M 5 5 C 4.2955948 5 3.6803238 5.3628126 3.3242188 5.9101562 L 14.292969 16.878906 C 14.696939 17.282876 15.303061 17.282876 15.707031 16.878906 L 26.675781 5.9101562 C 26.319676 5.3628126 25.704405 5 25 5 L 5 5 z M 3 8.4140625 L 3 23 C 3 24.108 3.892 25 5 25 L 25 25 C 26.108 25 27 24.108 27 23 L 27 8.4140625 L 17.121094 18.292969 C 15.958108 19.455959 14.041892 19.455959 12.878906 18.292969 L 3 8.4140625 z \" transform=\"translate(0,289.0625)\" id=\"rect4592\"/></g></svg>\n javascript\n get link() {\n const email = this.content || this.parent.get(\"email\")\n return email ? `mailto:${email}` : \"\"\n }\nhomeButtonParser\n popularity 0.006391\n description Home button.\n extends abstractIconButtonParser\n catchAllAtomType urlAtom\n string style left:2rem;\n string svg <svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12.7166 3.79541C12.2835 3.49716 11.7165 3.49716 11.2834 3.79541L4.14336 8.7121C3.81027 8.94146 3.60747 9.31108 3.59247 9.70797C3.54064 11.0799 3.4857 13.4824 3.63658 15.1877C3.7504 16.4742 4.05336 18.1747 4.29944 19.4256C4.41371 20.0066 4.91937 20.4284 5.52037 20.4284H8.84433C8.98594 20.4284 9.10074 20.3111 9.10074 20.1665V15.9754C9.10074 14.9627 9.90433 14.1417 10.8956 14.1417H13.4091C14.4004 14.1417 15.204 14.9627 15.204 15.9754V20.1665C15.204 20.3111 15.3188 20.4284 15.4604 20.4284H18.4796C19.0806 20.4284 19.5863 20.0066 19.7006 19.4256C19.9466 18.1747 20.2496 16.4742 20.3634 15.1877C20.5143 13.4824 20.4594 11.0799 20.4075 9.70797C20.3925 9.31108 20.1897 8.94146 19.8566 8.7121L12.7166 3.79541ZM10.4235 2.49217C11.3764 1.83602 12.6236 1.83602 13.5765 2.49217L20.7165 7.40886C21.4457 7.91098 21.9104 8.73651 21.9448 9.64736C21.9966 11.0178 22.0564 13.5119 21.8956 15.3292C21.7738 16.7067 21.4561 18.4786 21.2089 19.7353C20.9461 21.0711 19.7924 22.0001 18.4796 22.0001H15.4604C14.4691 22.0001 13.6655 21.1791 13.6655 20.1665V15.9754C13.6655 15.8307 13.5507 15.7134 13.4091 15.7134H10.8956C10.754 15.7134 10.6392 15.8307 10.6392 15.9754V20.1665C10.6392 21.1791 9.83561 22.0001 8.84433 22.0001H5.52037C4.20761 22.0001 3.05389 21.0711 2.79113 19.7353C2.54392 18.4786 2.22624 16.7067 2.10437 15.3292C1.94358 13.5119 2.00338 11.0178 2.05515 9.64736C2.08957 8.73652 2.55427 7.91098 3.28346 7.40886L10.4235 2.49217Z\"/></svg>\n javascript\n get link() {\n return this.content || this.get(\"link\") || \"index.html\"\n }\ntheScrollButtonParser\n popularity 0.006294\n description WWS button.\n extends abstractIconButtonParser\n string style position:relative;\n string svg <svg xmlns=\"http://www.w3.org/2000/svg\" direction=\"ltr\" width=\"231.52568231268793\" height=\"249.33156169975996\" viewBox=\"297.27169239534896 1273.121785992385 231.52568231268793 249.33156169975996\" stroke-linecap=\"round\" stroke-linejoin=\"round\" data-color-mode=\"dark\" class=\"tl-container tl-theme__force-sRGB tl-theme__dark\" ><defs/><g transform=\"matrix(1, 0, 0, 1, 395.9682, 1413.3618)\" opacity=\"1\"><g transform=\"scale(1)\"><path d=\"M-5.7342,-1.1484 T-5.0182,-4.6536 -2.6672,-12.9768 1.7374,-22.3903 8.4597,-30.0892 16.5455,-35.2796 25.3478,-37.8247 34.4641,-37.2199 43.0676,-33.4606 50.27,-27.1118 55.7861,-19.7972 59.8042,-11.6764 61.6844,-2.3157 60.8049,7.9615 57.3259,17.5954 51.2605,25.5515 41.9451,31.9237 30.6489,36.2084 18.9812,37.6038 8.4915,36.637 -1.3063,34.5174 -10.6869,31.6604 -19.0463,27.0252 -26.0612,20.1562 -31.864,11.1271 -35.2772,1.0066 -35.7963,-9.1933 -35.203,-19.2337 -32.52,-28.3259 -27.3388,-37.4245 -20.2857,-45.7085 -11.787,-53.0395 -2.4314,-58.4864 7.1724,-61.8739 17.5002,-65.6025 28.2859,-68.1034 38.514,-69.1081 48.1844,-69.9134 57.9136,-70.1787 67.4056,-69.5971 76.297,-67.7446 84.5233,-63.97 92.0158,-57.5003 98.1976,-48.5614 102.8839,-38.056 105.9936,-27.6097 107.074,-18.168 107.175,-8.2882 106.6452,1.9768 105.4734,11.7698 104.0279,21.1013 101.7439,31.0445 98.0977,40.6386 92.7822,49.1039 86.2847,57.2337 78.6099,64.7113 69.8339,70.974 61.2863,75.092 52.4283,78.2186 42.5338,80.7945 32.7733,82.6629 22.8568,83.6574 12.2803,83.3538 2.4971,81.4828 -6.3367,78.789 -15.1061,75.8011 -24.259,72.3779 -33.2286,68.991 -41.5707,64.8363 -49.153,59.1909 -55.8375,52.2938 -62.9311,43.9553 -68.4604,34.4882 -71.2761,23.5855 -72.748,12.7452 -73.7673,2.0453 -74.1187,-8.7386 -72.293,-19.3856 -68.7376,-30.4053 -64.12,-39.7852 -59.1825,-47.8931 -53.7695,-56.4512 -48.1998,-65.7676 -43.1389,-74.401 -36.9006,-81.0179 -28.4317,-86.2639 -18.3342,-90.3521 -8.9259,-93.9355 0.1927,-98.7179 9.3551,-103.3537 18.7651,-106.7552 29.7482,-110.3584 40.9287,-113.446 50.87,-115.2231 57.8279,-116.0074 60.4065,-116.0632 A7.8326,7.8326 0 0 1 61.1735,-100.4168 T58.6018,-100.2201 51.1634,-99.3008 41.695,-96.884 32.4044,-94.0187 22.65,-91.382 13.4974,-87.3756 5.0519,-83.1026 -5.0443,-78.6175 -15.7236,-74.4743 -24.6193,-70.2681 -31.274,-62.5903 -36.3827,-53.9362 -41.2833,-46.2905 -46.0679,-38.5161 -50.639,-30.6576 -54.6876,-22.4063 -57.6038,-12.3346 -58.5104,-1.8678 -57.6022,9.4969 -56.0628,20.477 -53.6434,29.0487 -48.8908,36.7517 -42.4075,43.9784 -35.3064,50.6236 -27.3553,55.2062 -17.6884,59.4064 -7.6442,63.6638 2.4427,67.0449 12.7308,69.4843 22.9294,70.2171 33.2547,69.3921 42.4972,67.7384 52.2753,64.8425 61.8649,60.9142 69.5292,56.0828 76.3604,49.5927 82.431,42.3194 87.219,34.619 90.6076,25.5217 92.547,16.0352 93.8703,7.1315 94.4972,-2.5268 94.3672,-13.3681 93.3204,-24.4252 90.5377,-34.1656 85.7321,-43.6441 78.7334,-51.8057 68.7542,-56.0559 57.9857,-57.2713 48.079,-57.0198 38.4535,-56.3204 28.5246,-55.0036 18.6104,-52.4796 9.6402,-49.559 0.7193,-46.2981 -7.1531,-41.3715 -13.708,-34.8957 -19.5435,-26.853 -22.8356,-17.0622 -23.363,-6.5446 -21.9969,3.1439 -17.1086,11.9113 -9.276,18.744 -0.0685,22.6058 9.3636,24.796 19.2422,25.8817 29.052,25.0411 37.9338,21.3502 45.6819,13.8498 49.6675,4.5228 49.2802,-5.1854 45.3277,-14.4455 38.8603,-21.7879 30.5488,-25.7753 21.3356,-24.3975 13.6687,-19.2391 8.8217,-10.9919 6.423,-2.3609 5.7342,1.1484 A5.8481,5.8481 0 0 1 -5.7342,-1.1484 Z\" stroke-linecap=\"round\"/></g></g></svg>\n javascript\n get link() {\n return \"https://wws.scroll.pub\"\n }\nabstractTextLinkParser\n extends abstractAftertextParser\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildTxt() {\n return this.text\n }\n buildHtml() {\n return `<div class=\"abstractTextLinkParser\"><a href=\"${this.link}\">${this.text}</a></div>`\n }\neditLinkParser\n popularity 0.001206\n extends abstractTextLinkParser\n description Print \"Edit\" link.\n string text Edit\n javascript\n get link() {\n return this.root.editUrl || \"\"\n }\nscrollVersionLinkParser\n popularity 0.006294\n extends abstractTextLinkParser\n string link https://scroll.pub\n description Print Scroll version.\n javascript\n get text() {\n return `Built with Scroll v${this.root.scrollVersion}`\n }\nclassicFormParser\n cue classicForm\n popularity 0.006391\n description Generate input form for ScrollSet.\n extends abstractAftertextParser\n atoms cueAtom\n catchAllAtomType stringAtom\n string script\n <script>\n sendFormViaEmail = form => {\n const mailto = new URL(\"mailto:\")\n const params = []\n const { value, title } = form.querySelector('button[type=\"submit\"]')\n params.push(`subject=${encodeURIComponent(value)}`)\n params.push(`to=${encodeURIComponent(title)}`)\n const oneTextarea = form.querySelector('textarea[title=\"oneTextarea\"]')\n const body = oneTextarea ? codeMirrorInstance.getValue() : Array.from(new FormData(form)).map(([name, value]) => `${name} ${value}`).join(\"\\\\n\")\n params.push(`body=${encodeURIComponent(body)}`)\n mailto.search = params.join(\"&\")\n window.open(mailto.href, '_blank')\n }\n </script>\n string style\n <style> .scrollFormParser {\n font-family: \"Gill Sans\", \"Bitstream Vera Sans\", sans-serif;\n }\n .scrollFormParser input , .scrollFormParser textarea{\n padding: 10px;\n margin-bottom: 10px;\n width: 100%;\n box-sizing: border-box;\n } .scrollFormParser label {\n display: block;\n margin-bottom: 5px;\n }\n </style>\n javascript\n get inputs() {\n return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {\n const {Name, Question, IsRequired, Type} = measure\n const type = Type || \"text\"\n const placeholder = Question\n const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)\n // ${index ? \"\" : \"autofocus\"}\n let tag = \"\"\n if (Type === \"textarea\")\n tag = `<textarea placeholder=\"${placeholder}\" id=\"${Name}\" name=\"${Name}\" ${IsRequired ? \"required\" : \"\"}></textarea>`\n else\n tag = `<input placeholder=\"${placeholder}\" type=\"${type}\" id=\"${Name}\" name=\"${Name}\" ${IsRequired ? \"required\" : \"\"}>`\n return `<div><label for=\"${Name}\" title=\"${IsRequired ? \"Required\" : \"\"}\">${ucFirst}${IsRequired ? \"*\" : \"\"}:</label>${tag}</div>`\n }).join(\"\\n\")\n }\n buildHtml() {\n const {isEmail, formDestination, callToAction, subject} = this\n return `${this.script}${this.style}<form ${isEmail ? \"onsubmit='sendFormViaEmail(this); return false;'\" : ` method='post' action='${formDestination}'`} class=\"scrollFormParser\">${this.inputs}<button value=\"${subject}\" title=\"${formDestination}\" class=\"scrollButton\" type=\"submit\">${callToAction}</button>${this.footer}</form>`\n }\n get callToAction() {\n return (this.isEmail ? \"Submit via email\" : (this.subject || \"Post\"))\n }\n get isEmail() {\n return this.formDestination.includes(\"@\")\n }\n get formDestination() {\n return this.getAtom(1) || \"\"\n }\n get subject() {\n return this.getAtomsFrom(2)?.join(\" \") || \"\"\n }\n get footer() {\n return \"\"\n }\nscrollFormParser\n extends classicFormParser\n cue scrollForm\n placeholderParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n valueParser\n atoms cueAtom\n baseParser blobParser\n cueFromId\n single\n nameParser\n description Name for the post submission.\n atoms cueAtom stringAtom\n cueFromId\n single\n description Generate a Scroll Form.\n string copyFromExternal codeMirror.css scrollLibs.js constants.js\n string requireOnce\n <link rel=\"stylesheet\" href=\"codeMirror.css\">\n <script src=\"scrollLibs.js\"></script>\n <script src=\"constants.js\"></script>\n javascript\n get placeholder() {\n return this.getParticle(\"placeholder\")?.subparticlesToString() || \"\"\n }\n get value() {\n return this.getParticle(\"value\")?.subparticlesToString() || \"\"\n }\n get footer() {\n return \"\"\n }\n get name() {\n return this.get(\"name\") || \"particles\"\n }\n get parsersBundle() {\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n const clone = this.root.clone()\n const parsers = clone.filter(line => parserRegex.test(line.getLine()))\n return \"\\n\" + parsers.map(particle => {\n particle.prependLine(\"boolean suggestInAutocomplete true\")\n return particle.toString()\n }).join(\"\\n\")\n }\n get inputs() {\n const Name = this.name\n return `<textarea title=\"oneTextarea\" rows=\"${Math.min(this.root.measures.length * 2, 30)}\" placeholder=\"${this.placeholder}\" id=\"${Name}\" name=\"${Name}\"></textarea>\n <script id=\"${Name}Parsers\" type=\"text/plain\">${this.parsersBundle}</script>\n <script>{\n const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm\n let {width, height} = document.getElementById('${Name}').getBoundingClientRect();\n const sp = new Particle(AppConstants.parsers)\n sp.filter(particle => particle.getLine().match(parserRegex)).forEach(part => {\n part.appendLine(\"boolean suggestInAutocomplete false\")\n })\n const customScrollParser = new HandParsersProgram(sp.toString() + document.getElementById(\"${Name}Parsers\").textContent).compileAndReturnRootParser()\n codeMirrorInstance = new ParsersCodeMirrorMode(\"custom\", () => customScrollParser, undefined, CodeMirror).register().fromTextAreaWithAutocomplete(document.getElementById(\"${Name}\"), {\n lineWrapping: false,\n lineNumbers: false\n })\n codeMirrorInstance.setSize(width, height);\n codeMirrorInstance.setValue(\\`${this.value}\\`); }</script>`\n }\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + super.buildHtml()\n }\nloremIpsumParser\n extends abstractAftertextParser\n cueFromId\n description Generate dummy text.\n catchAllAtomType integerAtom\n string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n javascript\n get originalText() {\n return this.placeholder.repeat(this.howMany)\n }\n get howMany() {\n return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1\n }\nnickelbackIpsumParser\n extends loremIpsumParser\n string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?\nprintSnippetsParser\n popularity 0.000338\n // todo: why are we extending AT here and not loops? Is it for class/id etc?\n extends abstractAftertextParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n description Prints snippets matching tag(s).\n example\n printSnippets index\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const {endSnippetIndex} = scrollProgram\n if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink\n const joinChar = \"\\n\"\n const html = scrollProgram\n .map((subparticle, index) => (index >= endSnippetIndex ? \"\" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(joinChar)\n .trim() +\n `<a class=\"scrollContinueReadingLink\" href=\"${linkRelativeToCompileTarget}\">Continue reading...</a>`\n return html\n }\n get files() {\n const thisFile = this.parent.file\n const files = this.root.getFilesByTags(this.content, this.has(\"limit\") ? parseInt(this.get(\"limit\")) : undefined).filter(file => file.file !== thisFile)\n // allow sortBy lastCommit Time\n if (this.get(\"sortBy\") === \"commitTime\") {\n return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()\n }\n return files\n }\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const snippets = this.files.map(file => {\n const buildSettings = {relativePath: file.relativePath, alreadyRequired }\n return `<div class=\"scrollSnippetContainer\">${this.makeSnippet(file.file.scrollProgram, buildSettings)}</div>`\n }).join(\"\\n\\n\")\n return `<div class=\"scrollColumns\" style=\"column-width:35ch;\">${snippets}</div>`\n }\n buildTxt() {\n return this.files.map(file => {\n const {scrollProgram} = file.file\n const {title, date, absoluteLink} = scrollProgram\n const ruler = \"=\".repeat(title.length)\n // Note: I tried to print the description here but the description generating code needs work.\n return `${title}\\n${ruler}\\n${date}\\n${absoluteLink}`\n }).join(\"\\n\\n\")\n }\nscrollNavParser\n popularity 0.000048\n extends printSnippetsParser\n cue nav\n description Titles and links in group(s).\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return `<nav class=\"scrollNavParser\">` + this.root.getFilesByTags(this.content).map(file => {\n const { linkTitle, permalink } = file.file.scrollProgram\n return `<a href=\"${permalink}\">${linkTitle}</a>` \n }).join(this.get(\"join\") || \" \") + `</nav>`\n }\nprintFullSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Print full pages in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml\n }\nprintShortSnippetsParser\n popularity 0.000048\n extends printSnippetsParser\n cueFromId\n description Titles and descriptions in group(s).\n javascript\n makeSnippet(scrollProgram, buildSettings) {\n const { title, permalink, description, timestamp } = scrollProgram\n return `<div><a href=\"${permalink}\">${title}</a><div>${description}...</div><div class=\"subdued\" style=\"text-align:right;\">${this.root.dayjs(timestamp * 1000).format(`MMMM D, YYYY`)}</div></div>`\n }\nprintRelatedParser\n popularity 0.001182\n description Print links to related posts.\n extends printSnippetsParser\n cueFromId\n javascript\n buildHtml() {\n const alreadyRequired = this.root.alreadyRequired\n const list = this.files.map(fileWrapper => {\n const {relativePath, file} = fileWrapper\n const {title, permalink, year} = file.scrollProgram\n return `- ${title}${year ? \" (\" + year + \")\" : \"\"}\\n link ${relativePath + permalink}`\n }).join(\"\\n\")\n const items = this.parent.concat(list)\n const html = items.map(item => item.buildHtml()).join(\"\\n\")\n items.forEach(item => item.destroy())\n return html\n }\nscrollNoticesParser\n extends abstractAftertextParser\n description Display messages in URL query parameters.\n cue notices\n javascript\n buildHtml() {\n const id = this.htmlId\n return `<div id=\"${id}\" class=\"scrollNoticesParser\" style=\"display:none;\"></div><script>(function(){\n const params = new URLSearchParams(window.location.search)\n if (!params.size) return\n document.getElementById('${id}').innerHTML = Array.from(params.entries()).map(([key, value]) => {\n if (key === \"error\") \n return '<div style=\"color:red\">Error: ' + value + '</div>'\n if (key === \"success\")\n return '<div style=\"color:green\">Success: ' + value + '</div>'\n return '<div>' + key + ': ' + value + '</div>'\n }).join(\"\") || \"No query parameters found\"\n document.getElementById('${id}').style.display = \"block\"\n })()</script>`\n }\nprintSourceStackParser\n // useful for debugging\n description Print compilation steps.\n extends abstractAftertextParser\n cueFromId\n example\n printOriginalSource\n javascript\n get sources() {\n const {file} = this.root\n const passNames = [\"codeAtStart\", \"fusedCode\", \"codeAfterMacroPass\"]\n let lastCode = \"\"\n return passNames.map(name => {\n let code = file[name]\n if (lastCode === code)\n code = \"[Unchanged]\"\n lastCode = file[name]\n return {\n name,\n code\n }})\n }\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.buildTxt().replace(/\\</g, \"<\")}</code>`\n }\n buildTxt() {\n return this.sources.map((pass, index) => `Pass ${index + 1} - ${pass.name}\\n========\\n${pass.code}`).join(\"\\n\\n\\n\")\n }\nabstractAssertionParser\n description Test above particle's output.\n extends abstractScrollParser\n string bindTo previous\n cueFromId\n javascript\n buildHtml() {\n return ``\n }\n get particleToTest() {\n // If the previous particle is also an assertion particle, use the one before that.\n return this.previous.particleToTest ? this.previous.particleToTest : this.previous\n }\n get actual() {return this.particleToTest.buildHtml()}\n getErrors() {\n const {actual} = this\n const expected = this.subparticlesToString()\n const errors = super.getErrors()\n if (this.areEqual(actual, expected))\n return errors\n return errors.concat(this.makeError(`'${actual}' did not ${this.kind} '${expected}'`))\n }\n catchAllParser htmlLineParser\nassertHtmlEqualsParser\n extends abstractAssertionParser\n string kind equal\n javascript\n areEqual(actual, expected) {\n return actual === expected\n }\n // todo: why are we having to super here?\n getErrors() { return super.getErrors()}\nassertBuildIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n get actual() { return this.particleToTest.buildOutput()}\n getErrors() { return super.getErrors()}\nassertHtmlIncludesParser\n extends abstractAssertionParser\n string kind include\n javascript\n areEqual(actual, expected) {\n return actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nassertHtmlExcludesParser\n extends abstractAssertionParser\n string kind exclude\n javascript\n areEqual(actual, expected) {\n return !actual.includes(expected)\n }\n getErrors() { return super.getErrors()}\nabstractPrintMetaParser\n extends abstractScrollParser\n cueFromId\nprintAuthorsParser\n popularity 0.001664\n description Prints author(s) byline.\n boolean isPopular true\n extends abstractPrintMetaParser\n // todo: we need pattern matching added to sdk to support having no params or a url and personNameAtom\n catchAllAtomType anyAtom\n example\n authors Breck Yunits\n https://breckyunits.com\n printAuthors\n javascript\n buildHtml() {\n return this.parent.getParticle(\"authors\")?.buildHtmlForPrint()\n }\n buildTxt() {\n return this.parent.getParticle(\"authors\")?.buildTxtForPrint()\n }\nprintDateParser\n popularity 0.000434\n extends abstractPrintMetaParser\n // If not present computes the date from the file's ctime.\n description Print published date.\n boolean isPopular true\n javascript\n buildHtml() {\n return `<div class=\"printDateParser\">${this.day}</div>`\n }\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\n buildTxt() {\n return this.day\n }\nprintFormatLinksParser\n description Prints links to other formats.\n extends abstractPrintMetaParser\n example\n printFormatLinks\n javascript\n buildHtml() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n // hacky\n const particle = this.appendSibling(`HTML | TXT`, `class printDateParser\\nlink ${permalink}.html HTML\\nlink ${permalink}.txt TXT\\nstyle text-align:center;`)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n const permalink = this.root.permalink.replace(\".html\", \"\")\n return `HTML | TXT\\n link ${permalink}.html HTML\\n link ${permalink}.txt TXT`\n }\nabstractBuildCommandParser\n extends abstractScrollParser\n cueFromId\n atoms buildCommandAtom\n catchAllAtomType filePathAtom\n inScope slashCommentParser\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\n get extension() {\n return this.cue.replace(\"build\", \"\")\n }\n buildOutput() {\n return this.root.compileTo(this.extension)\n }\n get outputFileNames() {\n return this.content?.split(\" \") || [this.root.permalink.replace(\".html\", \".\" + this.extension.toLowerCase())]\n }\n async _buildFileType(extension) {\n const {root} = this\n const { fileSystem, folderPath, filename, filePath, path, lodash } = root\n const capitalized = lodash.capitalize(extension)\n const buildKeyword = \"build\" + capitalized\n const {outputFileNames} = this\n for (let name of outputFileNames) {\n try {\n await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))\n root.log(`💾 Built ${name} from ${filename}`)\n } catch (err) {\n console.error(`Error while building '${filePath}' with extension '${extension}'`)\n throw err\n }\n }\n }\nabstractBuildOneCommandParser\n // buildOne and buildTwo are just a dumb/temporary way to have CSVs/JSONs/TSVs build first. Will be merged at some point.\n extends abstractBuildCommandParser\n javascript\n async buildOne() { await this._buildFileType(this.extension) }\nbuildCsvParser\n popularity 0.000096\n description Compile to CSV file.\n extends abstractBuildOneCommandParser\nbuildTsvParser\n popularity 0.000096\n description Compile to TSV file.\n extends abstractBuildOneCommandParser\nbuildJsonParser\n popularity 0.000096\n description Compile to JSON file.\n extends abstractBuildOneCommandParser\nabstractBuildTwoCommandParser\n extends abstractBuildCommandParser\n javascript\n async buildTwo() {\n await this._buildFileType(this.extension)\n }\nbuildCssParser\n popularity 0.000048\n description Compile to CSS file.\n extends abstractBuildTwoCommandParser\nbuildHtmlParser\n popularity 0.007645\n description Compile to HTML file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\n javascript\n async buildTwo(externalFilesCopied) {\n await this._copyExternalFiles(externalFilesCopied)\n await super.buildTwo()\n }\n async _copyExternalFiles(externalFilesCopied = {}) {\n if (!this.isNodeJs()) return\n const {root} = this\n // If this file uses a parser that has external requirements,\n // copy those from external folder into the destination folder.\n const { parsersRequiringExternals, folderPath, fileSystem, filename, parserIdIndex, path, Disk, externalsPath } = root\n if (!externalFilesCopied[folderPath]) externalFilesCopied[folderPath] = {}\n parsersRequiringExternals.forEach(parserId => {\n if (externalFilesCopied[folderPath][parserId]) return\n if (!parserIdIndex[parserId]) return\n parserIdIndex[parserId].map(particle => {\n const externalFiles = particle.copyFromExternal.split(\" \")\n externalFiles.forEach(name => {\n const newPath = path.join(folderPath, name)\n fileSystem.writeProduct(newPath, Disk.read(path.join(externalsPath, name)))\n root.log(`💾 Copied external file needed by ${filename} to ${name}`)\n })\n })\n if (parserId !== \"scrollThemeParser\")\n // todo: generalize when not to cache\n externalFilesCopied[folderPath][parserId] = true\n })\n }\nbuildJsParser\n description Compile to JS file.\n extends abstractBuildTwoCommandParser\nbuildRssParser\n popularity 0.000048\n description Write RSS file.\n extends abstractBuildTwoCommandParser\nbuildTxtParser\n popularity 0.007596\n description Compile to TXT file.\n extends abstractBuildTwoCommandParser\n boolean isPopular true\nloadConceptsParser\n // todo: clean this up. just add smarter imports with globs?\n // this currently removes any \"import\" statements.\n description Import all concepts in a folder.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom filePathAtom\n javascript\n async load() {\n const { Disk, path, importRegex } = this.root\n const folder = path.join(this.root.folderPath, this.getAtom(1))\n const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(\".scroll\")).map(Disk.read).join(\"\\n\\n\").replace(importRegex, \"\")\n this.parent.concat(ONE_BIG_FILE)\n //console.log(ONE_BIG_FILE)\n }\n buildHtml() {\n return \"\"\n }\nbuildConceptsParser\n popularity 0.000024\n cueFromId\n description Write concepts to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom anyAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileConcepts(link, sortBy))\n root.log(`💾 Built concepts in ${filename} to ${link}`)\n }\n }\nbuildParsersParser\n popularity 0.000096\n description Compile to Parsers file.\n extends abstractBuildCommandParser\nfetchParser\n description Download URL to disk.\n extends abstractBuildCommandParser\n cueFromId\n atoms preBuildCommandAtom urlAtom\n example\n fetch https://breckyunits.com/posts.csv\n fetch https://breckyunits.com/posts.csv renamed.csv\n javascript\n get url() {\n return this.getAtom(1)\n }\n get filename() {\n return this.getAtom(2)\n }\n async load() {\n await this.root.fetch(this.url, this.filename)\n }\n buildHtml() {\n return \"\"\n }\nbuildMeasuresParser\n popularity 0.000024\n cueFromId\n description Write measures to csv+ files.\n extends abstractBuildCommandParser\n sortByParser\n cueFromId\n atoms cueAtom anyAtom\n javascript\n async buildOne() {\n const {root} = this\n const { fileSystem, folderPath, filename, path, permalink } = root\n const files = this.getAtomsFrom(1)\n if (!files.length) files.push(permalink.replace(\".html\", \".csv\"))\n const sortBy = this.get(\"sortBy\")\n for (let link of files) {\n await fileSystem.writeProduct(path.join(folderPath, link), root.compileMeasures(link, sortBy))\n root.log(`💾 Built measures in ${filename} to ${link}`)\n }\n }\nbuildPdfParser\n popularity 0.000096\n description Compile to PDF file.\n extends abstractBuildCommandParser\n javascript\n async buildTwo() {\n if (!this.isNodeJs()) return \"Only works in Node currently.\"\n const {root} = this\n const { filename } = root\n const outputFile = root.filenameNoExtension + \".pdf\"\n // relevant source code for chrome: https://github.com/chromium/chromium/blob/a56ef4a02086c6c09770446733700312c86f7623/components/headless/command_handler/headless_command_switches.cc#L22\n const command = `/Applications/Google\\\\ Chrome.app/Contents/MacOS/Google\\\\ Chrome --headless --disable-gpu --no-pdf-header-footer --default-background-color=00000000 --no-pdf-background --print-to-pdf=\"${outputFile}\" \"${this.permalink}\"`\n // console.log(`Node.js is running on architecture: ${process.arch}`)\n try {\n const output = require(\"child_process\").execSync(command, { stdio: \"ignore\" })\n root.log(`💾 Built ${outputFile} from ${filename}`)\n } catch (error) {\n console.error(error)\n }\n }\nabstractTopLevelSingleMetaParser\n description Use these parsers once per file.\n extends abstractScrollParser\n inScope slashCommentParser\n cueFromId\n atoms metaCommandAtom\n javascript\n isTopMatter = true\n isSetterParser = true\n buildHtml() {\n return \"\"\n }\ntestStrictParser\n description Make catchAllParagraphParser = error.\n extends abstractTopLevelSingleMetaParser\nscrollDateParser\n cue date\n popularity 0.006680\n catchAllAtomType dateAtom\n description Set published date.\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\n example\n date 1/11/2019\n printDate\n Hello world\n dateline\nabstractUrlSettingParser\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom urlAtom\n cueFromId\neditBaseUrlParser\n popularity 0.007838\n description Override edit link baseUrl.\n extends abstractUrlSettingParser\ncanonicalUrlParser\n description Override canonical URL.\n extends abstractUrlSettingParser\nopenGraphImageParser\n popularity 0.000796\n // https://ogp.me/\n // If not defined, Scroll will try to generate it's own using the first image tag on your page.\n description Override Open Graph Image.\n extends abstractUrlSettingParser\nbaseUrlParser\n popularity 0.009188\n description Required for RSS and OpenGraph.\n extends abstractUrlSettingParser\nrssFeedUrlParser\n popularity 0.008850\n description Set RSS feed URL.\n extends abstractUrlSettingParser\neditUrlParser\n catchAllAtomType urlAtom\n description Override edit link.\n extends abstractTopLevelSingleMetaParser\nsiteOwnerEmailParser\n popularity 0.001302\n description Set email address for site contact.\n extends abstractTopLevelSingleMetaParser\n cue email\n atoms metaCommandAtom emailAddressAtom\nfaviconParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue favicon\n description Favicon file.\n example\n favicon logo.png\n metatags\n buildHtml\n extends abstractTopLevelSingleMetaParser\nimportOnlyParser\n popularity 0.033569\n // This line will be not be imported into the importing file.\n description Don't build this file.\n cueFromId\n atoms preBuildCommandAtom\n extends abstractTopLevelSingleMetaParser\n javascript\n buildHtml() {\n return \"\"\n }\ninlineMarkupsParser\n popularity 0.000024\n description Set global inline markups.\n extends abstractTopLevelSingleMetaParser\n cueFromId\n example\n inlineMarkups\n * \n // Disable * for bold\n _ u\n // Make _ underline\nhtmlLangParser\n atoms metaCommandAtom stringAtom\n // for the <html lang=\"\"> tag. If not specified will be \"en\". See https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang\n description Override HTML lang attribute.\n extends abstractTopLevelSingleMetaParser\nopenGraphDescriptionParser\n popularity 0.001688\n catchAllAtomType stringAtom\n cue description\n description Meta tag description.\n extends abstractTopLevelSingleMetaParser\npermalinkParser\n popularity 0.000265\n description Override output filename.\n extends abstractTopLevelSingleMetaParser\n atoms metaCommandAtom permalinkAtom\nscrollTagsParser\n popularity 0.006801\n cue tags\n description Set tags.\n example\n tags All\n extends abstractTopLevelSingleMetaParser\n catchAllAtomType tagAtom\nscrollTitleParser\n popularity 0.007524\n catchAllAtomType anyAtom\n cue title\n description Set title.\n example\n title Eureka\n printTitle\n extends abstractTopLevelSingleMetaParser\n boolean isPopular true\nscrollLinkTitleParser\n popularity 0.007524\n catchAllAtomType anyAtom\n cue linkTitle\n description Text for links.\n example\n title My blog - Eureka\n linkTitle Eureka\n extends abstractTopLevelSingleMetaParser\nscrollChatParser\n popularity 0.000362\n description A faux text chat conversation.\n catchAllParser chatLineParser\n cue chat\n extends abstractScrollParser\n example\n chat\n Hi\n 👋\n javascript\n buildHtml() {\n return this.map((line, index) => line.asString ? `<div style=\"text-align: ${index % 2 ? \"right\" : \"left\"};\" class=\"scrollChat ${index % 2 ? \"scrollChatRight\" : \"scrollChatLeft\"}\"><span>${line.asString}</span></div>` : \"\").join(\"\")\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nabstractDatatableProviderParser\n description A datatable.\n extends abstractScrollParser\n inScope scrollTableDataParser scrollTableDelimiterParser abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser\n javascript\n get visualizations() {\n return this.topDownArray.filter(particle => particle.isTableVisualization || particle.isHeader || particle.isHtml)\n }\n buildHtml(buildSettings) {\n return this.visualizations.map(particle => particle.buildHtml(buildSettings))\n .join(\"\\n\")\n .trim()\n }\n buildTxt() {\n return this.visualizations.map(particle => particle.buildTxt())\n .join(\"\\n\")\n .trim()\n }\n _coreTable\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n return []\n }\n get columnNames() {\n return []\n }\nscrollTableParser\n extends abstractDatatableProviderParser\n popularity 0.002133\n cue table\n example\n table\n printTable\n data\n year,count\n 1900,10\n 2000,122\n 2020,23\n catchAllAtomType filePathAtom\n int atomIndex 1\n javascript\n get delimiter() {\n const {filename} = this\n let delimiter = \"\"\n if (filename) {\n const extension = filename.split(\".\").pop()\n if (extension === \"json\") delimiter = \"json\"\n if (extension === \"particles\") delimiter = \"particles\"\n if (extension === \"csv\") delimiter = \",\"\n if (extension === \"tsv\") delimiter = \"\\t\"\n if (extension === \"ssv\") delimiter = \" \"\n if (extension === \"psv\") delimiter = \"|\"\n }\n if (this.get(\"delimiter\"))\n delimiter = this.get(\"delimiter\")\n else if (!delimiter) {\n const header = this.delimitedData.split(\"\\n\")[0]\n if (header.includes(\"\\t\"))\n delimiter = \"\\t\"\n else if (header.includes(\",\"))\n delimiter = \",\"\n else\n delimiter = \" \"\n }\n return delimiter\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const {delimiter, delimitedData} = this\n if (delimiter === \"json\") {\n const obj = JSON.parse(delimitedData)\n let rows = []\n // Optimal case: Array of objects\n if (Array.isArray(obj)) { rows = obj}\n else if (!Array.isArray(obj) && typeof obj === \"object\") {\n // Case 2: Nested array under a key\n const arrayKey = Object.keys(obj).find(key => Array.isArray(obj[key]))\n if (arrayKey) rows = obj[arrayKey]\n }\n // Case 3: Array of primitive values\n else if (Array.isArray(obj) && obj.length && typeof obj[0] !== \"object\") {\n rows = obj.map(value => ({ value }))\n }\n this._columnNames = rows.length ? Object.keys(rows[0]) : []\n this._coreTable = rows\n return rows\n }\n else if (delimiter === \"particles\") {\n const d3lib = typeof d3 === \"undefined\" ? require('d3') : d3\n this._coreTable = d3lib.dsvFormat(\",\").parse(new Particle(delimitedData).asCsv, d3lib.autoType)\n } else {\n const d3lib = typeof d3 === \"undefined\" ? require('d3') : d3\n this._coreTable = d3lib.dsvFormat(delimiter).parse(delimitedData, d3lib.autoType)\n }\n this._columnNames = this._coreTable.columns\n delete this._coreTable.columns\n return this._coreTable\n }\n get columnNames() {\n // init coreTable to set columns\n const coreTable = this.coreTable\n return this._columnNames\n }\n async load() {\n if (this.filename)\n await this.root.fetch(this.filename)\n }\n get fileContent() {\n return this.root.readSyncFromFileOrUrl(this.filename)\n }\n get delimitedData() {\n // json csv tsv\n if (this.filename)\n return this.fileContent\n const dataParticle = this.getParticle(\"data\")\n if (dataParticle)\n return dataParticle.subparticlesToString()\n // if not dataparticle and no filename, check [permalink].csv\n if (this.isNodeJs())\n return this.root.readFile(this.root.permalink.replace(\".html\", \"\") + \".csv\")\n return \"\"\n }\nclocParser\n extends scrollTableParser\n description Output results of cloc as table.\n cue cloc\n string copyFromExternal clocLangs.txt\n javascript\n delimiter = \",\"\n get delimitedData() {\n const { execSync } = require(\"child_process\")\n const results = execSync(this.command).toString().trim()\n const csv = results.split(\"\\n\\n\").pop().replace(/,\\\"github\\.com\\/AlDanial.+/, \"\") // cleanup output\n return csv\n }\n get command(){\n return `cloc --vcs git . --csv --read-lang-def=clocLangs.txt ${this.content || \"\"}`\n }\nscrollDependenciesParser\n extends scrollTableParser\n description Get files this file depends on.\n cue dependencies\n javascript\n delimiter = \",\"\n get delimitedData() {\n return `file\\n` + this.root.dependencies.join(\"\\n\")\n }\nscrollDiskParser\n extends scrollTableParser\n description Output file into as table.\n cue disk\n javascript\n delimiter = \"json\"\n get delimitedData() {\n return this.isNodeJs() ? this.delimitedDataNodeJs : \"\"\n }\n get delimitedDataNodeJs() {\n const fs = require('fs');\n const path = require('path');\n const {folderPath} = this.root\n const folder = this.content ? path.join(folderPath, this.content) : folderPath\n function getDirectoryContents(dirPath) {\n const directoryContents = [];\n const items = fs.readdirSync(dirPath);\n items.forEach((item) => {\n const itemPath = path.join(dirPath, item);\n const stats = fs.statSync(itemPath);\n directoryContents.push({\n name: item,\n type: stats.isDirectory() ? 'directory' : 'file',\n size: stats.size,\n lastModified: stats.mtime\n });\n });\n return directoryContents;\n }\n return JSON.stringify(getDirectoryContents(folder))\n }\nquickTableParser\n popularity 0.000024\n extends scrollTableParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(tsv|csv|ssv|psv|json)[^\\s]*$\n int atomIndex 0\n javascript\n get dependencies() { return [this.cue]}\nscrollIrisParser\n extends scrollTableParser\n description Iris dataset from R.A. Fisher.\n cue iris\n example\n iris\n printTable\n scatter\n x SepalLength\n y SepalWidth\n javascript\n delimitedData = this.constructor.iris\nscrollConceptsParser\n description Load concepts as table.\n extends abstractDatatableProviderParser\n cue concepts\n atoms cueAtom\n example\n concepts\n printTable\n javascript\n get coreTable() {\n return this.root.concepts\n }\n get columnNames() {\n return this.root.measures.map(col => col.Name)\n }\nabstractPostsParser\n description Load posts as table.\n extends abstractDatatableProviderParser\n cueFromId\n atoms cueAtom\n catchAllAtomType tagWithOptionalFolderAtom\n javascript\n async load() {\n const dependsOn = this.tags.map(tag => this.root.parseNestedTag(tag)).filter(i => i).map(i => i.folderPath)\n const {fileSystem} = this.root\n for (let folderPath of dependsOn) {\n // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.fusionId}'`)\n await fileSystem.getLoadedFilesInFolder(folderPath, \".scroll\")\n }\n }\n get tags() {\n return this.content?.split(\" \") || []\n }\n get files() {\n const thisFile = this.root.file\n // todo: we can include this file, but just not run asTxt\n const files = this.root.getFilesByTags(this.tags).filter(file => file.file !== thisFile)\n return files\n }\n get coreTable() {\n if (this._coreTable) return this._coreTable\n this._coreTable = this.files.map(file => this.postToRow(file))\n return this._coreTable\n }\n postToRow(file) {\n const {relativePath} = file\n const {scrollProgram} = file.file\n const {title, permalink, asTxt, date, wordCount, minutes} = scrollProgram\n const text = asTxt.replace(/(\\t|\\n)/g, \" \").replace(/</g, \"<\")\n return {\n title, titleLink: relativePath + permalink, text, date, wordCount, minutes\n }\n }\n columnNames = \"title titleLink text date wordCount minutes\".split(\" \")\nscrollPostsParser\n popularity 0.000024\n cue posts\n description Posts as table.\n extends abstractPostsParser\n example\n // Print a search table:\n posts\n printTable\n tableSearch\n // Dump a CSV of blog:\n buildHtml\n buildCsv\n buildJson\nscrollPostsMetaParser\n popularity 0.000024\n cue postsMeta\n description Post meta as table.\n extends abstractPostsParser\n javascript\n columnNames = [\"date\", \"year\", \"title\", \"permalink\", \"authors\", \"tags\", \"wordCount\", \"minutes\"]\n postToRow(file) {\n const {date, year, title, permalink, authors, tags, wordCount, minutes} = file.file.scrollProgram\n return {\n date, year, title, permalink, authors, tags, wordCount, minutes\n }\n }\nprintFeedParser\n popularity 0.000048\n description Print group to RSS.\n extends abstractPostsParser\n example\n printFeed index\n printFeed cars/index\n buildRss\n javascript\n buildRss() {\n const {dayjs} = this.root\n const scrollPrograms = this.files.map(file => file.file.scrollProgram)\n const { title, baseUrl, description } = this.root\n return `<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>\n <rss version=\"2.0\">\n <channel>\n <title>${title}</title>\n <link>${baseUrl}</link>\n <description>${description}</description>\n <lastBuildDate>${dayjs().format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}</lastBuildDate>\n <language>en-us</language>\n ${scrollPrograms.map(program => program.toRss()).join(\"\\n\")}\n </channel>\n </rss>`\n }\n buildTxt() {\n return this.buildRss()\n }\nprintSourceParser\n popularity 0.000024\n description Print source for files in group(s).\n extends printFeedParser\n example\n printSource index\n buildTxt source.txt\n javascript\n buildHtml() {\n const files = this.root.getFilesByTags(this.content).map(file => file.file)\n return `${files.map(file => file.filePath + \"\\n \" + file.codeAtStart.replace(/\\n/g, \"\\n \") ).join(\"\\n\")}`\n }\nprintSiteMapParser\n popularity 0.000072\n extends abstractPostsParser\n description Print text sitemap.\n example\n baseUrl http://test.com\n printSiteMap\n javascript\n buildHtml() {\n const { baseUrl } = this.root\n return this.files.map(file => baseUrl + file.relativePath + file.file.scrollProgram.permalink).join(\"\\n\")\n }\n buildTxt() {\n return this.buildHtml()\n }\n get dependencies() { return this.files}\ncodeParser\n popularity 0.001929\n description A code block.\n catchAllParser lineOfCodeParser\n extends abstractScrollParser\n boolean isPopular true\n example\n code\n two = 1 + 1\n javascript\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.code.replace(/\\</g, \"<\")}</code>`\n }\n buildTxt() {\n return \"```\\n\" + this.code + \"\\n```\"\n }\n get code() {\n return this.subparticlesToString()\n }\n cueFromId\ncodeWithHeaderParser\n popularity 0.000169\n cueFromId\n catchAllAtomType stringAtom\n extends codeParser\n example\n codeWithHeader math.py\n two = 1 + 1\n javascript\n buildHtml() {\n return `<div class=\"codeWithHeader\"><div class=\"codeHeader\">${this.content}</div>${super.buildHtml()}</div>`\n }\n buildTxt() {\n return \"```\" + this.content + \"\\n\" + this.code + \"\\n```\"\n }\ncodeFromFileParser\n popularity 0.000169\n cueFromId\n atoms cueAtom urlAtom\n extends codeWithHeaderParser\n example\n codeFromFile math.py\n javascript\n get code() {\n return this.root.readSyncFromFileOrUrl(this.content)\n }\ncodeWithLanguageParser\n popularity 0.000458\n description Use this to specify the language of the code block, such as csvCode or rustCode.\n extends codeParser\n pattern ^[a-zA-Z0-9_]+Code$\nabstractScrollWithRequirementsParser\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n return this.getHtmlRequirements(buildSettings) + this.buildInstance()\n }\ncopyButtonsParser\n popularity 0.001471\n extends abstractScrollWithRequirementsParser\n description Copy code widget.\n javascript\n buildInstance() {\n return \"\"\n }\n string requireOnce\n <script>\n document.addEventListener(\"DOMContentLoaded\", () => document.querySelectorAll(\".scrollCodeBlock\").forEach(block =>\n {\n if (!navigator.clipboard) return\n const button = document.createElement(\"span\")\n button.classList.add(\"scrollCopyButton\")\n block.appendChild(button)\n button.addEventListener(\"click\", async () => {\n await navigator.clipboard.writeText(block.innerText)\n button.classList.add(\"scrollCopiedButton\")\n })\n }\n ))\n </script>\nabstractTableVisualizationParser\n extends abstractScrollWithRequirementsParser\n boolean isTableVisualization true\n javascript\n get columnNames() {\n return this.parent.columnNames\n }\nheatrixParser\n cueFromId\n example\n heatrix\n '2007 '2008 '2009 '2010 '2011 '2012 '2013 '2014 '2015 '2016 '2017 '2018 '2019 '2020 '2021 '2022 '2023 '2024\n 4 11 23 37 3 14 12 0 0 0 5 1 2 11 15 10 12 56\n description A heatmap matrix data visualization.\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n javascript\n buildHtml() {\n // A hacky but simple way to do this for now.\n const advanced = new Particle(\"heatrixAdvanced\")\n advanced.appendLineAndSubparticles(\"table\", \"\\n \" + this.tableData.replace(/\\n/g, \"\\n \"))\n const particle = this.appendSibling(\"heatrixAdvanced\", advanced.subparticlesToString())\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n get tableData() {\n const {coreTable} = this.parent\n if (!coreTable)\n return this.subparticlesToString()\n let table = new Particle(coreTable).asSsv\n if (this.parent.cue === \"transpose\") {\n // drop first line after transpose\n const lines = table.split(\"\\n\")\n lines.shift()\n table = lines.join(\"\\n\")\n }\n // detect years and make strings\n const lines = table.split(\"\\n\")\n const yearLine = / \\d{4}(\\s+\\d{4})+$/\n if (yearLine.test(lines[0])) {\n lines[0] = lines[0].replace(/ /g, \" '\")\n table = lines.join(\"\\n\")\n }\n return table\n }\nheatrixAdvancedParser\n popularity 0.000048\n cueFromId\n catchAllParser heatrixCatchAllParser\n extends abstractTableVisualizationParser\n description Advanced heatrix.\n example\n heatrix\n table\n \n %h10; '2007 '2008 '2009\n 12 4 323\n scale\n #ebedf0 0\n #c7e9c0 100\n #a1d99b 400\n #74c476 1600\n javascript\n buildHtml() {\n class Heatrix {\n static HeatrixId = 0\n uid = Heatrix.HeatrixId++\n constructor(program) {\n const isDirective = atom => /^(f|l|w|h)\\d+$/.test(atom) || atom === \"right\" || atom === \"left\" || atom.startsWith(\"http://\") || atom.startsWith(\"https://\") || atom.endsWith(\".html\")\n const particle = new Particle(program)\n this.program = particle\n const generateColorBinningString = (data, colors) => {\n const sortedData = [...data].sort((a, b) => a - b);\n const n = sortedData.length;\n const numBins = colors.length;\n // Calculate the indices for each quantile\n const indices = [];\n for (let i = 1; i < numBins; i++) {\n indices.push(Math.floor((i / numBins) * n));\n }\n // Get the quantile values and round them\n const thresholds = indices.map(index => Math.round(sortedData[index]));\n // Generate the string\n let result = '';\n colors.forEach((color, index) => {\n const threshold = index === colors.length - 1 ? thresholds[index - 1] * 2 : thresholds[index];\n result += `${color} ${threshold}\\n`;\n });\n return result.trim();\n }\n const buildScale = (table) => {\n const numbers = table.split(\"\\n\").map(line => line.split(\" \")).flat().filter(atom => !isDirective(atom)).map(atom => parseFloat(atom)).filter(number => !isNaN(number))\n const colors = ['#ebedf0', '#c7e9c0', '#a1d99b', '#74c476', '#41ab5d', '#238b45', '#005a32'];\n numbers.unshift(0)\n return generateColorBinningString(numbers, colors);\n }\n const table = particle.getParticle(\"table\").subparticlesToString()\n const scale = particle.getParticle(\"scale\")?.subparticlesToString() || buildScale(table)\n const thresholds = []\n const colors = []\n scale.split(\"\\n\").map((line) => {\n const parts = line.split(\" \")\n thresholds.push(parseFloat(parts[1]))\n colors.push(parts[0])\n })\n const colorCount = colors.length\n const colorFunction = (value) => {\n if (isNaN(value)) return \"\" // #ebedf0\n for (let index = 0; index < colorCount; index++) {\n const threshold = thresholds[index]\n if (value <= threshold) return colors[index]\n }\n return colors[colorCount - 1]\n }\n const directiveDelimiter = \";\"\n const getSize = (directives, letter) =>\n directives\n .filter((directive) => directive.startsWith(letter))\n .map((dir) => dir.replace(letter, \"\") + \"px\")[0] ?? \"\"\n this.table = table.split(\"\\n\").map((line) =>\n line\n .trimEnd()\n .split(\" \")\n .map((atom) => {\n const atoms = atom.split(directiveDelimiter).filter((atom) => !isDirective(atom)).join(\"\")\n const directivesInThisAtom = atom\n .split(directiveDelimiter)\n .filter(isDirective)\n const value = parseFloat(atoms)\n const label = atoms.includes(\"'\") ? atoms.split(\"'\")[1] : atoms\n const alignment = directivesInThisAtom.includes(\"right\")\n ? \"right\"\n : directivesInThisAtom.includes(\"left\")\n ? \"left\"\n : \"\"\n const color = colorFunction(value)\n const width = getSize(directivesInThisAtom, \"w\")\n const height = getSize(directivesInThisAtom, \"h\")\n const fontSize = getSize(directivesInThisAtom, \"f\")\n const lineHeight = getSize(directivesInThisAtom, \"l\") || height\n const link = directivesInThisAtom.filter(i => i.startsWith(\"http\") || i.endsWith(\".html\"))[0]\n const style = {\n \"background-color\": color,\n width,\n height,\n \"font-size\": fontSize,\n \"line-height\": lineHeight,\n \"text-align\": alignment,\n }\n Object.keys(style).filter(key => !style[key]).forEach((key) => delete style[key])\n return {\n value,\n label,\n style,\n link,\n }\n })\n )\n }\n get html() {\n const { program } = this\n const cssId = `#heatrix${this.uid}`\n const defaultWidth = \"40px\"\n const defaultHeight = \"40px\"\n const fontSize = \"10px\"\n const lineHeight = defaultHeight\n const style = `<style>\n .heatrixContainer {\n margin: auto;\n }.heatrixRow {white-space: nowrap;}\n ${cssId} .heatrixAtom {\n font-family: arial;\n border-radius: 2px;\n border: 1px solid transparent;\n display: inline-block;\n margin: 1px;\n text-align: center;\n vertical-align: middle;\n overflow: hidden;\n text-overflow: ellipsis;\n }\n .heatrixAtom a {\n color: black;\n }\n ${cssId} .heatrixAtom{\n width: ${defaultWidth};\n height: ${defaultHeight};\n font-size: ${fontSize};\n line-height: ${lineHeight};\n }\n </style>`\n const firstRow = this.table[0]\n return (\n `<div class=\"heatrixContainer\" id=\"heatrix${this.uid}\">${style}` +\n this.table\n .map((row, rowIndex) => {\n if (!rowIndex) return \"\"\n const rowStyle = row[0].style\n return `<div class=\"heatrixRow heatrixRow${rowIndex}\">${row\n .map((atom, columnIndex) => {\n if (!columnIndex) return \"\"\n const columnStyle = firstRow[columnIndex]?.style || {}\n let { value, label, style, link } = atom\n const extendedStyle = Object.assign(\n {},\n rowStyle,\n columnStyle,\n style\n )\n const inlineStyle = Object.keys(extendedStyle)\n .map((key) => `${key}:${extendedStyle[key]};`)\n .join(\"\")\n let valueClass = value ? \" valueAtom\" : \"\"\n const href = link ? ` href=\"${link}\"` : \"\"\n return `<div class=\"heatrixAtom heatrixColumn${columnIndex}${valueClass}\" style=\"${inlineStyle}\"><a title=\"${label}\" ${href}>${label}</a></div>`\n })\n .join(\"\")}</div>`\n })\n .join(\"\\n\") +\n \"</div>\"\n ).replace(/\\n/g, \"\")\n }\n }\n return new Heatrix(this.subparticlesToString().trim()).html\n }\nmapParser\n latParser\n atoms cueAtom floatAtom\n cueFromId\n single\n longParser\n atoms cueAtom floatAtom\n cueFromId\n single\n tilesParser\n atoms cueAtom tileOptionAtom\n cueFromId\n single\n zoomParser\n atoms cueAtom integerAtom\n cueFromId\n single\n geolocateParser\n description Geolocate user.\n atoms cueAtom\n cueFromId\n single\n radiusParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillOpacityParser\n atoms cueAtom floatAtom\n cueFromId\n single\n fillColorParser\n atoms cueAtom anyAtom\n cueFromId\n single\n colorParser\n atoms cueAtom anyAtom\n cueFromId\n single\n heightParser\n atoms cueAtom floatAtom\n cueFromId\n single\n hoverParser\n atoms cueAtom\n catchAllAtomType anyAtom\n cueFromId\n single\n extends abstractTableVisualizationParser\n description Map widget.\n string copyFromExternal leaflet.css leaflet.js scrollLibs.js\n string requireOnce\n <link rel=\"stylesheet\" href=\"leaflet.css\">\n <script src=\"leaflet.js\"></script>\n <script src=\"scrollLibs.js\"></script>\n javascript\n buildInstance() {\n const height = this.get(\"height\") || 500\n const id = this._getUid()\n const obj = this.toObject()\n const template = {}\n const style = height !== \"full\" ? `height: ${height}px;` : `height: 100%; position: fixed; z-index: -1; left: 0; top: 0; width: 100%;`\n const strs = [\"color\", \"fillColor\"]\n const nums = [\"radius\", \"fillOpacity\"]\n strs.filter(i => obj[i]).forEach(i => template[i] = obj[i])\n nums.filter(i => obj[i]).forEach(i => template[i] = parseFloat(obj[i]))\n const mapId = `map${id}`\n return `<div id=\"${mapId}\" style=\"${style}\"></div>\n <script>\n {\n if (!window.maps) window.maps = {}\n const moveToMyLocation = () => {\n if (!navigator.geolocation) return\n navigator.geolocation.getCurrentPosition((position) => {\n const { latitude, longitude } = position.coords\n window.maps.${mapId}.setView([latitude, longitude])\n L.circleMarker([latitude, longitude], {\n fillOpacity: 0.8,\n radius: 20,\n weight: 4\n })\n .addTo(window.maps.${mapId})\n }, () => {})\n }\n const lat = ${this.get(\"lat\") ?? 37.8}\n const long = ${this.get(\"long\") ?? 4}\n if (${this.has(\"geolocate\")}){\n moveToMyLocation()\n }\n const zoomLevel = ${this.get(\"zoom\") ?? 4}\n const hover = '${this.get(\"hover\") || \"<b>{title}</b><br>{description}\"}'\n const template = ${JSON.stringify(template)}\n const points = ${JSON.stringify((this.parent.coreTable || []).filter(point => point.lat && point.long), undefined, 2)}\n window.maps.${mapId} = L.map(\"map${id}\").setView([lat, long], zoomLevel)\n const map = window.maps.${mapId}\n const tileOptions = {\n \"default\": {\n baseLayer: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',\n attribution: '<a href=\"https://www.openstreetmap.org/\">OpenStreetMap</a> contributors'\n },\n light: {\n baseLayer: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',\n attribution: '<a href=\"https://www.openstreetmap.org/\">OpenStreetMap</a> contributors <a href=\"https://carto.com/ attributions\">CARTO</a>'\n },\n }\n const {baseLayer, attribution} = tileOptions.${this.get(\"tiles\") || \"default\"}\n L.tileLayer(baseLayer, {\n attribution,\n maxZoom: 19\n }).addTo(map);\n points.forEach(point => {\n L.circleMarker([point.lat, point.long], {...template, ...Object.fromEntries(\n Object.entries(point).filter(([key, value]) => value !== null)\n )})\n .addTo(map)\n .bindPopup(new Particle(point).evalTemplateString(hover))\n })\n }\n </script>`\n }\nabstractPlotParser\n // Observablehq\n extends abstractTableVisualizationParser\n string copyFromExternal d3.js plot.js\n string requireOnce\n <script src=\"d3.js\"></script>\n <script src=\"plot.js\"></script>\n example\n plot\n inScope abstractColumnNameParser\n javascript\n buildInstance() {\n const id = \"plot\" + this._getUid()\n return `<div id=\"${id}\"></div><script>\n {\n let loadChart = async () => {\n const data = ${this.dataCode}\n const get = (col, index ) => col !== \"undefined\" ? col : (index === undefined ? undefined : Object.keys(data[0])[index])\n document.querySelector(\"#${id}\").append(Plot.plot(${this.plotOptions}))\n }\n loadChart()\n }\n </script>`\n }\n get marks() {\n // just for testing purposes\n return `Plot.rectY({length: 10000}, Plot.binX({y: \"count\"}, {x: d3.randomNormal()}))`\n }\n get dataCode() {\n const {coreTable} = this.parent\n return `d3.csvParse(\\`${new Particle(coreTable).asCsv}\\`, d3.autoType)`\n }\n get plotOptions() {\n return `{\n title: \"${this.get(\"title\") || \"\"}\",\n subtitle: \"${this.get(\"subtitle\") || \"\"}\",\n caption: \"${this.get(\"caption\") || \"\"}\",\n symbol: {legend: ${this.has(\"symbol\")}},\n color: {legend: ${this.has(\"fill\")}},\n grid: ${this.get(\"grid\") !== \"false\"},\n marks: [${this.marks}],\n }`\n }\nscatterplotParser\n extends abstractPlotParser\n description Scatterplot Widget.\n // todo: make copyFromExternal work with inheritance\n string copyFromExternal d3.js plot.js\n javascript\n get marks() {\n const x = this.get(\"x\")\n const y = this.get(\"y\")\n const text = this.get(\"label\")\n return `Plot.dot(data, {\n x: get(\"${x}\", 0),\n y: get(\"${y}\", 1),\n r: get(\"${this.get(\"radius\")}\"),\n fill: get(\"${this.get(\"fill\")}\"),\n tip: true,\n symbol: get(\"${this.get(\"symbol\")}\")} ), Plot.text(data, {x: get(\"${x}\",0), y: get(\"${y}\", 1), text: \"${text}\", dy: -6, lineAnchor: \"bottom\"})`\n }\nsparklineParser\n popularity 0.000024\n description Sparkline widget.\n extends abstractTableVisualizationParser\n example\n sparkline 1 2 3 4 5\n string copyFromExternal sparkline.js\n string requireOnce <script src=\"sparkline.js\"></script>\n catchAllAtomType numberAtom\n // we need pattern matching\n inScope scrollYParser\n javascript\n buildInstance() {\n const id = \"spark\" + this._getUid()\n const {columnValues} = this\n const start = this.has(\"start\") ? parseInt(this.get(\"start\")) : 0\n const width = this.get(\"width\") || 100\n const height = this.get(\"height\") || 30\n const lineColor = this.get(\"color\") || \"black\"\n return `<span id=\"${id}\"></span><script>new Sparkline(document.getElementById(\"${id}\"), {dotRadius: 0, width: ${width}, height: ${height}, lineColor: \"${lineColor}\", tooltip: (value,index) => ${start} + index + \": \" + value}).draw(${JSON.stringify(columnValues)})</script>`\n }\n get columnValues() {\n if (this.content)\n return this.content.split(\" \").map(str => parseFloat(str))\n const {coreTable} = this.parent\n if (coreTable) {\n const columnName = this.get(\"y\") || Object.keys(coreTable[0]).find(key => typeof coreTable[0][key] === 'number')\n return coreTable.map(row => row[columnName])\n }\n }\nprintColumnParser\n popularity 0.000024\n description Print one column\n extends abstractTableVisualizationParser\n example\n printColumn tags\n catchAllAtomType columnNameAtom\n joinParser\n boolean allowTrailingWhitespace true\n cueFromId\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.columnValues.join(this.join)\n }\n buildTxt() {\n return this.columnValues.join(this.join)\n }\n get join() {\n return this.get(\"join\") || \"\\n\"\n }\n get columnName() {\n return this.atoms[1]\n }\n get columnValues() {\n return this.parent.coreTable.map(row => row[this.columnName])\n }\nprintTableParser\n popularity 0.001085\n cueFromId\n description Print table.\n extends abstractTableVisualizationParser\n javascript\n get tableHeader() {\n return this.columns.filter(col => !col.isLink).map(column => `<th>${column.name}</th>\\n`)\n }\n get columnNames() {\n return this.parent.columnNames\n }\n buildJson() {\n return JSON.stringify(this.coreTable, undefined, 2)\n }\n buildCsv() {\n return new Particle(this.coreTable).asCsv\n }\n buildTsv() {\n return new Particle(this.coreTable).asTsv\n }\n get columns() {\n const {columnNames} = this\n return columnNames.map((name, index) => {\n const isLink = name.endsWith(\"Link\")\n const linkIndex = columnNames.indexOf(name + \"Link\")\n return {\n name,\n isLink,\n linkIndex\n }\n })\n }\n toRow(row) {\n const {columns} = this\n const atoms = columns.map(col => row[col.name])\n let str = \"\"\n let column = 0\n const columnCount = columns.length\n while (column < columnCount) {\n const col = columns[column]\n column++\n const content = ((columnCount === column ? atoms.slice(columnCount - 1).join(\" \") : atoms[column - 1]) ?? \"\").toString()\n if (col.isLink) continue\n const isTimestamp = col.name.toLowerCase().includes(\"time\") && /^\\d{10}(\\d{3})?$/.test(content)\n const text = isTimestamp ? new Date(parseInt(content.length === 10 ? content * 1000 : content)).toLocaleString() : content\n let tagged = text\n const link = atoms[col.linkIndex]\n const isUrl = content.match(/^https?\\:[^ ]+$/)\n if (col.linkIndex > -1 && link) tagged = `<a href=\"${link}\">${text}</a>`\n else if (col.name.endsWith(\"Url\")) tagged = `<a href=\"${content}\">${col.name.replace(\"Url\", \"\")}</a>`\n else if (isUrl) tagged = `<a href=\"${content}\">${text}</a>`\n str += `<td>${tagged}</td>\\n`\n }\n return str\n }\n get coreTable() {\n return this.parent.coreTable\n }\n get tableBody() {\n return this.coreTable\n .map(row => `<tr>${this.toRow(row)}</tr>`)\n .join(\"\\n\")\n }\n buildHtml() {\n return `<table id=\"table${this._getUid()}\" class=\"scrollTable\">\n <thead><tr>${this.tableHeader.join(\"\\n\")}</tr></thead>\n <tbody>${this.tableBody}</tbody>\n </table>`\n }\n buildTxt() {\n return this.parent.delimitedData || new Particle(this.coreTable).asCsv\n }\nkatexParser\n popularity 0.001592\n extends abstractScrollWithRequirementsParser\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\n example\n katex\n \\text{E} = \\text{T} / \\text{A}!\n description KaTex widget for typeset math.\n string copyFromExternal katex.min.css katex.min.js\n string requireOnce\n <link rel=\"stylesheet\" href=\"katex.min.css\">\n <script defer src=\"katex.min.js\"></script>\n <script>\n document.addEventListener(\"DOMContentLoaded\", () => document.querySelectorAll(\".scrollKatex\").forEach(el =>\n {\n katex.render(el.innerText, el, {\n throwOnError: false\n });\n }\n ))\n </script>\n javascript\n buildInstance() {\n const id = this._getUid()\n const content = this.content === undefined ? \"\" : this.content\n return `<div class=\"scrollKatex\" id=\"${id}\">${content + this.subparticlesToString()}</div>`\n }\n buildTxt() {\n return ( this.content ? this.content : \"\" )+ this.subparticlesToString()\n }\nhelpfulNotFoundParser\n popularity 0.000048\n extends abstractScrollWithRequirementsParser\n catchAllAtomType filePathAtom\n string copyFromExternal helpfulNotFound.js\n description Helpful not found widget.\n javascript\n buildInstance() {\n return `<style>#helpfulNotFound{margin: 100px 0;}</style><h1 id=\"helpfulNotFound\"></h1><script defer src=\"/helpfulNotFound.js\"></script><script>document.addEventListener(\"DOMContentLoaded\", () => new NotFoundApp('${this.content}'))</script>`\n }\nslideshowParser\n // Left and right arrows navigate.\n description Slideshow widget. *** delimits slides.\n extends abstractScrollWithRequirementsParser\n string copyFromExternal jquery-3.7.1.min.js slideshow.js\n example\n slideshow\n Why did the cow cross the road?\n ***\n Because it wanted to go to the MOOOO-vies.\n ***\n THE END\n ****\n javascript\n buildHtml() {\n return `<style>html {font-size: var(--scrollBaseFontSize, 28px);} body {margin: auto; width: 500px;}.slideshowNav{text-align: center; margin-bottom:20px; font-size: 24px;color: rgba(204,204,204,.8);} a{text-decoration: none; color: rgba(204,204,204,.8);}</style><script defer src=\"jquery-3.7.1.min.js\"></script><div class=\"slideshowNav\"></div><script defer src=\"slideshow.js\"></script>`\n }\ntableSearchParser\n popularity 0.000072\n extends abstractScrollWithRequirementsParser\n string copyFromExternal jquery-3.7.1.min.js datatables.css dayjs.min.js datatables.js tableSearch.js\n string requireOnce\n <script defer src=\"jquery-3.7.1.min.js\"></script>\n <style>.dt-search{font-family: \"SF Pro\", \"Helvetica Neue\", \"Segoe UI\", \"Arial\";}</style>\n <link rel=\"stylesheet\" href=\"datatables.css\">\n <script defer src=\"datatables.js\"></script>\n <script defer src=\"dayjs.min.js\"></script>\n <script defer src=\"tableSearch.js\"></script>\n // adds to all tables on page\n description Table search and sort widget.\n javascript\n buildInstance() {\n return \"\"\n }\nabstractCommentParser\n description Prints nothing.\n catchAllAtomType commentAtom\n atoms commentAtom\n extends abstractScrollParser\n baseParser blobParser\n string bindTo next\n javascript\n buildHtml() {\n return ``\n }\n catchAllParser commentLineParser\ncommentParser\n popularity 0.000193\n extends abstractCommentParser\n cueFromId\ncounterpointParser\n description Counterpoint comment. Prints nothing.\n extends commentParser\n cue !\nslashCommentParser\n popularity 0.005643\n extends abstractCommentParser\n cue //\n boolean isPopular true\n description A comment. Prints nothing.\nthanksToParser\n description Acknowledgements comment. Prints nothing.\n extends abstractCommentParser\n cueFromId\nscrollClearStackParser\n popularity 0.000096\n cue clearStack\n description Clear body stack.\n extends abstractScrollParser\n boolean isHtml true\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return this.root.clearBodyStack().trim()\n }\ncssParser\n popularity 0.007211\n extends abstractScrollParser\n description A style tag.\n cueFromId\n catchAllParser cssLineParser\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>${this.css}</style>`\n }\n get css() {\n return this.content ?? this.subparticlesToString()\n }\n buildCs() {\n return this.css\n }\ninlineCssParser\n description Inline CSS from files.\n popularity 0.007211\n extends abstractScrollParser\n cueFromId\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return `<style>/* ${this.content} */\\n${this.css}</style>`\n }\n get css() {\n return this.atoms.slice(1).map(filename => this.root.readFile(filename)).join(\"\\n\\n\")\n }\n buildCs() {\n return this.css\n }\nscrollBackgroundColorParser\n description Quickly set CSS background.\n popularity 0.007211\n extends abstractScrollParser\n cue background\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>html, body { background: ${this.content};}</style>`\n }\nscrollFontColorParser\n description Quickly set CSS font-color.\n popularity 0.007211\n extends abstractScrollParser\n cue color\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n return `<style>html, body { color: ${this.content};}</style>`\n }\nscrollFontParser\n description Quickly set font family.\n popularity 0.007211\n extends abstractScrollParser\n cue font\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n javascript\n buildHtml() {\n const font = this.content === \"Slim\" ? \"Helvetica Neue; font-weight:100;\" : this.content\n return `<style>html, body, h1,h2,h3 { font-family: ${font};}</style>`\n }\nabstractQuickIncludeParser\n popularity 0.007524\n extends abstractScrollParser\n atoms urlAtom\n javascript\n get dependencies() { return [this.filename]}\n get filename() {\n return this.getAtom(0)\n }\nquickCssParser\n popularity 0.007524\n description Make a CSS tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(css)$\n javascript\n buildHtml() {\n return `<link rel=\"stylesheet\" type=\"text/css\" href=\"${this.filename}\">`\n }\nquickIncludeHtmlParser\n popularity 0.007524\n description Include an HTML file.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(html|htm)$\n javascript\n buildHtml() {\n return this.root.readFile(this.filename)\n }\nquickScriptParser\n popularity 0.007524\n description Make a Javascript tag.\n extends abstractQuickIncludeParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(js)$\n javascript\n buildHtml() {\n return `<script src=\"${this.filename}\"></script>`\n }\nscrollDashboardParser\n popularity 0.000145\n description Key stats in large font.\n catchAllParser lineOfCodeParser\n cue dashboard\n extends abstractScrollParser\n example\n dashboard\n #2 Popularity\n 30 Years Old\n $456 Revenue\n javascript\n get tableBody() {\n const items = this.topDownArray\n let str = \"\"\n for (let i = 0; i < items.length; i = i + 3) {\n str += this.makeRow(items.slice(i, i + 3))\n }\n return str\n }\n makeRow(items) {\n return `<tr>` + items.map(particle => `<td>${particle.cue}<span>${particle.content}</span></td>`).join(\"\\n\") + `</tr>\\n`\n }\n buildHtml() {\n return `<table class=\"scrollDashboard\">${this.tableBody}</table>`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nscrollDefParser\n popularity 0.004244\n description Parser short form.\n pattern ^[a-zA-Z0-9_]+Def\n extends abstractScrollParser\n catchAllAtomType stringAtom\n example\n urlDef What is the URL?\n javascript\n buildParsers(index) {\n const idStuff = index ? \"\" : `boolean isMeasure true\n boolean isMeasureRequired true\n boolean isConceptDelimiter true`\n const description = this.content\n const cue = this.cue.replace(\"Def\", \"\")\n const sortIndex = 1 + index/10\n return `${cue}DefParser\n cue ${cue}\n extends abstractStringMeasureParser\n description ${description}\n float sortIndex ${sortIndex}\n ${idStuff}`.trim()\n }\nbelowAsCodeParser\n popularity 0.000651\n description Print code below.\n string bindTo next\n extends abstractScrollParser\n catchAllAtomType integerAtom\n cueFromId\n javascript\n method = \"next\"\n get selectedParticles() {\n const { method } = this\n let code = \"\"\n let particles = []\n let next = this[method]\n let {howMany} = this\n while (howMany) {\n particles.push(next)\n next = next[method]\n howMany--\n }\n if (this.reverse) particles.reverse()\n return particles\n }\n get code() {\n return this.selectedParticles.map(particle => particle.asString).join(\"\\n\")\n }\n reverse = false\n buildHtml() {\n return `<code class=\"scrollCodeBlock\">${this.code.replace(/\\</g, \"<\")}</code>`\n }\n get howMany() {\n let howMany = parseInt(this.getAtom(1))\n if (!howMany || isNaN(howMany)) howMany = 1\n return howMany\n }\nbelowAsCodeUntilParser\n description Print code above until match.\n extends belowAsCodeParser\n catchAllAtomType anyAtom\n example\n belowAsCode\n counter 1 second\n javascript\n get howMany() {\n let howMany = 1\n const query = this.content\n let particle = this.next\n while (particle !== this) {\n if (particle.getLine().startsWith(query))\n return howMany\n particle = particle.next\n howMany++\n }\n return howMany\n }\naboveAsCodeParser\n popularity 0.000482\n string bindTo previous\n description Print code above.\n example\n counter 1 second\n aboveAsCode\n extends belowAsCodeParser\n javascript\n method = \"previous\"\n reverse = true\ninspectBelowParser\n description Inspect particle below.\n extends belowAsCodeParser\n string copyFromExternal inspector.css\n javascript\n get code() {\n const mapFn = particle => {\n const atomTypes = particle.lineAtomTypes.split(\" \")\n return `<div class=\"inspectorParticle\"><span class=\"inspectorParticleId\">${particle.constructor.name}</span>${particle.atoms.map((atom, index) => `<span class=\"inspectorAtom\">${atom}<span class=\"inspectorAtomType\">${atomTypes[index]}</span></span>`).join(\" \")}${(particle.length ? `<br><div class=\"inspectorSubparticles\">` + particle.map(mapFn).join(\"<br>\") + `</div>` : \"\")}</div>`}\n return this.selectedParticles.map(mapFn).join(\"<br>\")\n }\n buildHtml() {\n return `<link rel=\"stylesheet\" href=\"inspector.css\">` + this.code\n }\ninspectAboveParser\n description Inspect particle above.\n extends inspectBelowParser\n string bindTo previous\n javascript\n method = \"previous\"\n reverse = true\nhakonParser\n cueFromId\n extends abstractScrollParser\n description Compile Hakon to CSS.\n catchAllParser hakonContentParser\n javascript\n buildHtml() {\n return `<style>${this.css}</style>`\n }\n get css() {\n const {hakonParser} = this.root\n return new hakonParser(this.subparticlesToString()).compile()\n }\n buildCs() {\n return this.css\n }\nhamlParser\n popularity 0.007524\n description HTML tag via HAML syntax.\n extends abstractScrollParser\n atoms urlAtom\n catchAllAtomType stringAtom\n pattern ^%?[\\w\\.]+#[\\w\\.]+ *\n javascript\n get tag() {\n return this.atoms[0].split(/[#\\.]/).shift().replace(\"%\", \"\")\n }\n get htmlId() {\n const idMatch = this.atoms[0].match(/#([\\w-]+)/)\n return idMatch ? idMatch[1] : \"\"\n }\n get htmlClasses() {\n return this.atoms[0].match(/\\.([\\w-]+)/g)?.map(cls => cls.slice(1)) || [];\n }\n buildHtml() {\n const {htmlId, htmlClasses, content, tag} = this\n this.parent.sectionStack.unshift(`</${tag}>`)\n const attrs = [htmlId ? ' id=\"' + htmlId + '\"' : \"\", htmlClasses.length ? ' class=\"' + htmlClasses.join(\" \") + '\"' : \"\"].join(\" \").trim()\n return `<${tag}${attrs ? \" \" + attrs : \"\"}>${content || \"\"}`\n }\n buildTxt() {\n return this.content\n }\nhamlTagParser\n // Match plain tags like %h1\n extends hamlParser\n pattern ^%[^#]+$\nabstractHtmlParser\n extends abstractScrollParser\n catchAllParser htmlLineParser\n catchAllAtomType htmlAnyAtom\n javascript\n buildHtml() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\n buildTxt() {\n return \"\"\n }\nhtmlParser\n popularity 0.000048\n extends abstractHtmlParser\n description HTML one liners or blocks.\n cueFromId\nhtmlInlineParser\n popularity 0.005788\n extends abstractHtmlParser\n atoms htmlAnyAtom\n boolean isHtml true\n pattern ^<\n description Inline HTML.\n boolean isPopular true\n javascript\n buildHtml() {\n return `${this.getLine() ?? \"\"}${this.subparticlesToString()}`\n }\nscrollBrParser\n popularity 0.000096\n cue br\n description A break.\n extends abstractScrollParser\n catchAllAtomType integerAtom\n boolean isHtml true\n javascript\n buildHtml() {\n return `<br>`.repeat(parseInt(this.getAtom(1) || 1))\n }\niframesParser\n popularity 0.000121\n cueFromId\n catchAllAtomType urlAtom\n extends abstractScrollParser\n description An iframe(s).\n example\n iframes frame.html\n javascript\n buildHtml() {\n return this.atoms.slice(1).map(url => `<iframe src=\"${url}\" frameborder=\"0\"></iframe>`).join(\"\\n\")\n }\nabstractCaptionedParser\n extends abstractScrollParser\n atoms cueAtom urlAtom\n inScope captionAftertextParser slashCommentParser\n cueFromId\n javascript\n buildHtml(buildSettings) {\n const caption = this.getParticle(\"caption\")\n const captionFig = caption ? `<figcaption>${caption.buildHtml()}</figcaption>` : \"\"\n const {figureWidth} = this\n const widthStyle = figureWidth ? `width:${figureWidth}px; margin: auto;` : \"\"\n const float = this.has(\"float\") ? `margin: 20px; float: ${this.get(\"float\")};` : \"\"\n return `<figure class=\"scrollCaptionedFigure\" style=\"${widthStyle + float}\">${this.getFigureContent(buildSettings)}${captionFig}</figure>`\n }\n get figureWidth() {\n return this.get(\"width\")\n }\nscrollImageParser\n cue image\n popularity 0.005908\n description An img tag.\n boolean isPopular true\n extends abstractCaptionedParser\n int atomIndex 1\n example\n image screenshot.png\n caption A caption.\n inScope classMarkupParser aftertextIdParser linkParser linkTargetParser openGraphParser\n javascript\n get dimensions() {\n const width = this.get(\"width\")\n const height = this.get(\"height\")\n if (width || height)\n return {width, height}\n if (!this.isNodeJs())\n return {}\n const src = this.filename\n // If its a local image, get the dimensions and put them in the HTML\n // to avoid flicker\n if (src.startsWith(\"http:\") || src.startsWith(\"https:\")) return {}\n if (this._dimensions)\n return this._dimensions\n try {\n const sizeOf = require(\"image-size\")\n const path = require(\"path\")\n const fullImagePath = path.join(this.root.folderPath, src)\n this._dimensions = sizeOf(fullImagePath)\n return this._dimensions\n } catch (err) {\n console.error(err)\n }\n return {}\n }\n get figureWidth() {\n return this.dimensions.width\n }\n get filename() {\n return this.getAtom(this.atomIndex)\n }\n get dependencies() { return [this.filename]}\n getFigureContent(buildSettings) {\n const linkRelativeToCompileTarget = (buildSettings ? (buildSettings.relativePath ?? \"\") : \"\") + this.filename\n const {width, height} = this.dimensions\n let dimensionAttributes = width || height ? `width=\"${width}\" height=\"${height}\" ` : \"\"\n // Todo: can we reuse more code from aftertext?\n const className = this.has(\"class\") ? ` class=\"${this.get(\"class\")}\" ` : \"\"\n const id = this.has(\"id\") ? ` id=\"${this.get(\"id\")}\" ` : \"\"\n const clickLink = this.find(particle => particle.definition.isOrExtendsAParserInScope([\"linkParser\"])) || linkRelativeToCompileTarget \n const target = this.has(\"target\") ? this.get(\"target\") : (this.has(\"link\") ? \"\" : \"_blank\")\n return `<a href=\"${clickLink}\" target=\"${target}\" ${className} ${id}><img src=\"${linkRelativeToCompileTarget}\" ${dimensionAttributes}loading=\"lazy\"></a>`\n }\n buildTxt() {\n const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join(\"\\n\")\n return \"[Image Omitted]\" + (subparticles ? \"\\n \" + subparticles.replace(/\\n/g, \"\\n \") : \"\")\n }\nquickImageParser\n popularity 0.005788\n extends scrollImageParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(jpg|jpeg|png|gif|webp|svg|bmp)\n int atomIndex 0\nqrcodeParser\n extends abstractCaptionedParser\n description Make a QR code from a link.\n example\n qrcode https://scroll.pub\n javascript\n getFigureContent() {\n const url = this.atoms[1]\n const isNode = this.isNodeJs()\n if (isNode) {\n const {externalsPath} = this.root\n const path = require(\"path\")\n const {qrcodegen, toSvgString} = require(path.join(externalsPath, \"qrcodegen.js\"))\n const QRC = qrcodegen.QrCode;\n const qr0 = QRC.encodeText(url, QRC.Ecc.MEDIUM);\n const svg = toSvgString(qr0, 4); // See qrcodegen-input-demo\n return svg\n }\n return `Not yet supported in browser.`\n }\nyoutubeParser\n popularity 0.000121\n extends abstractCaptionedParser\n // Include the YouTube embed URL such as https://www.youtube.com/embed/CYPYZnVQoLg\n description A YouTube video widget.\n example\n youtube https://www.youtube.com/watch?v=lO8blNtYYBA\n javascript\n getFigureContent() {\n const url = this.getAtom(1).replace(\"youtube.com/watch?v=\", \"youtube.com/embed/\")\n return `<div class=\"scrollYouTubeHolder\"><iframe class=\"scrollYouTubeEmbed\" src=\"${url}\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe></div>`\n }\nyouTubeParser\n extends youtubeParser\n tags deprecate\n // Deprecated. You youtube all lowercase.\nimportParser\n description Import a file.\n popularity 0.007524\n cueFromId\n atoms preBuildCommandAtom\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n example\n import header.scroll\nscrollImportedParser\n description Inserted at import pass.\n boolean suggestInAutocomplete false\n cue imported\n atoms preBuildCommandAtom\n extends abstractScrollParser\n baseParser blobParser\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return \"\"\n }\n getErrors() {\n if (this.get(\"exists\") === \"false\" && this.previous.getLine() !== \"// optional\")\n return [this.makeError(`File '${this.atoms[1]}' does not exist.`)]\n return []\n }\nquickImportParser\n popularity 0.007524\n description Import a Scroll or Parsers file.\n extends abstractScrollParser\n boolean isPopular true\n atoms urlAtom\n pattern ^[^\\s]+\\.(scroll|parsers)$\n javascript\n buildHtml() {\n return \"\"\n }\n example\n header.scroll\ninlineJsParser\n description Inline JS from files.\n popularity 0.007211\n extends abstractScrollParser\n cueFromId\n catchAllAtomType filePathAtom\n javascript\n buildHtml() {\n return `<script>/* ${this.content} */\\n${this.contents}</script>`\n }\n get contents() {\n return this.atoms.slice(1).map(filename => this.root.readFile(filename)).join(\";\\n\\n\")\n }\n buildJs() {\n return this.contents\n }\nscriptParser\n extends abstractScrollParser\n description Print script tag.\n cueFromId\n catchAllParser scriptLineParser\n catchAllAtomType scriptAnyAtom\n javascript\n buildHtml() {\n return `<script>${this.scriptContent}</script>`\n }\n get scriptContent() {\n return this.content ?? this.subparticlesToString()\n }\n buildJs() {\n return this.scriptContent\n }\njsonScriptParser\n popularity 0.007524\n cueFromId\n description Include JSON and assign to window.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n javascript\n buildHtml() {\n const varName = this.filename.split(\"/\").pop().replace(\".json\", \"\")\n return `<script>window.${varName} = ${this.root.readFile(this.filename)}</script>`\n }\n get filename() {\n return this.getAtom(1)\n }\nscrollLeftRightButtonsParser\n popularity 0.006342\n cue leftRightButtons\n description Previous and next nav buttons.\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const { linkToPrevious, linkToNext } = this.root\n if (!linkToPrevious) return \"\"\n const style = `a.keyboardNav {display:block;position:absolute;top:0.25rem; color: rgba(204,204,204,.8); font-size: 1.875rem; line-height: 1.7rem;}a.keyboardNav:hover{color: #333;text-decoration: none;}`\n return `<style>${style}</style><a class=\"keyboardNav doNotPrint\" style=\"left:.5rem;\" href=\"${linkToPrevious}\"><</a><a class=\"keyboardNav doNotPrint\" style=\"right:.5rem;\" href=\"${linkToNext}\">></a>`\n }\nkeyboardNavParser\n popularity 0.007476\n description Make left and right navigate files.\n extends abstractScrollParser\n cueFromId\n catchAllAtomType urlAtom\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const linkToPrevious = this.getAtom(1) ?? root.linkToPrevious\n const linkToNext = this.getAtom(2) ?? root.linkToNext\n const script = `<script>document.addEventListener('keydown', function(event) {\n if (document.activeElement !== document.body) return\n if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return // dont interfere with keyboard back button shortcut\n const getLinks = () => document.getElementsByClassName(\"scrollKeyboardNav\")[0].getElementsByTagName(\"a\")\n if (event.key === \"ArrowLeft\")\n getLinks()[0].click()\n else if (event.key === \"ArrowRight\")\n getLinks()[1].click()\n });</script>`\n return `<div class=\"scrollKeyboardNav\" style=\"display:none;\"><a href=\"${linkToPrevious}\">${linkToPrevious}</a> · ${root.permalink} · <a href=\"${linkToNext}\">${linkToNext}</a>${script}</div>`\n }\nprintUsageStatsParser\n popularity 0.000096\n // todo: if we include the atom \"Parser\" in a cue, bad things seem to happen.\n description Parser usage stats for folder.\n extends abstractScrollParser\n cueFromId\n javascript\n get stats() {\n const input = this.root.allScrollFiles.map(file => file.scrollProgram).map(program => program.parserIds.join(\"\\n\")).join(\"\\n\")\n const result = input.split('\\n').reduce((acc, atom) => (acc[atom] = (acc[atom] || 0) + 1, acc), {})\n const rows = Object.entries(result).map(([atom, count]) => { return {atom, count}})\n const sorted = this.root.lodash.sortBy(rows, \"count\").reverse()\n return \"parserId uses\\n\" + sorted.map(row => `${row.atom} ${row.count}`).join('\\n')\n }\n buildHtml() {\n // A hacky but simple way to do this for now.\n const particle = this.appendSibling(\"table\")\n particle.appendLine(\"delimiter \")\n particle.appendLine(\"printTable\")\n const dataParticle = particle.appendLine(\"data\")\n dataParticle.setSubparticles(this.stats)\n const html = particle.buildHtml()\n particle.destroy()\n return html\n }\n buildTxt() {\n return this.stats\n }\n buildCsv() {\n return this.stats.replace(/ /g, \",\")\n }\nprintScrollLeetSheetParser\n popularity 0.000024\n description Print Scroll parser leet sheet.\n extends abstractScrollParser\n tags experimental\n cueFromId\n javascript\n get parsersToDocument() {\n const clone = this.root.clone()\n clone.setSubparticles(\"\")\n const atoms = clone.getAutocompleteResultsAt(0,0).matches.map(a => a.text)\n atoms.push(\"blankline\") // manually add blank line\n atoms.push(\"Catch All Paragraph.\") // manually add catch all paragraph\n atoms.push(\"<h></h>\") // manually add html\n atoms.sort()\n clone.setSubparticles(atoms.join(\"\\n\").replace(/blankline/, \"\")) // insert blank line in right spot\n return clone\n }\n sortDocs(docs) {\n return docs.map(particle => {\n const {definition} = particle\n const {id, description, isPopular, examples, popularity} = definition\n const tags = definition.get(\"tags\") || \"\"\n if (tags.includes(\"deprecate\") || tags.includes(\"experimental\"))\n return null\n const category = this.getCategory(tags)\n const note = this.getNote(category)\n return {id: definition.cueIfAny || id, description, isPopular, examples, note, popularity: Math.ceil(parseFloat(popularity) * 100000)}\n }).filter(i => i).sort((a, b) => a.id.localeCompare(b.id))\n }\n makeLink(examples, cue) {\n // if (!examples.length) console.log(cue) // find particles that need docs\n const example = examples.length ? examples[0].subparticlesToString() : cue\n const base = `https://try.scroll.pub/`\n const particle = new Particle()\n particle.appendLineAndSubparticles(\"scroll\", \"theme gazette\\n\" + example)\n return base + \"#\" + encodeURIComponent(particle.asString)\n }\n docToHtml(doc) {\n const css = `#scrollLeetSheet {color: grey;} #scrollLeetSheet a {color: #3498db; }`\n return `<style>${css}</style><div id=\"scrollLeetSheet\">` + doc.map(obj => `<div class=\"${obj.category}\"><a href=\"${this.makeLink(obj.examples, obj.id)}\">${obj.isPopular ? \"<b>\" : \"\"}${obj.id}</a> ${obj.description}${obj.isPopular ? \"</b>\" : \"\"}${obj.note}</div>`).join(\"\\n\") + \"</div>\"\n }\n buildHtml() {\n return this.docToHtml(this.sortDocs(this.parsersToDocument))\n }\n buildTxt() {\n return this.sortDocs(this.parsersToDocument).map(obj => `${obj.id} - ${obj.description}`).join(\"\\n\")\n }\n getCategory(input) {\n return \"\"\n }\n getNote() {\n return \"\"\n }\n buildCsv() {\n const rows = this.sortDocs(this.parsersToDocument).map(obj => {\n const {id, isPopular, description, popularity, category} = obj\n return {\n id,\n isPopular,\n description,\n popularity,\n category\n }\n })\n return new Particle(this.root.lodash.sortBy(rows, \"isPopular\")).asCsv\n }\nprintparsersLeetSheetParser\n popularity 0.000024\n // todo: fix parse bug when atom Parser appears in parserId\n extends printScrollLeetSheetParser\n tags experimental\n description Parsers leetsheet.\n javascript\n buildHtml() {\n return \"<p><b>Parser Definition Parsers</b> define parsers that acquire, analyze and act on code.</p>\" + this.docToHtml(this.sortDocs(this.parsersToDocument)) + \"<p><b>Atom Definition Parsers</b> analyze the atoms in a line.</p>\" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))\n }\n makeLink() {\n return \"\"\n }\n categories = \"assemblePhase acquirePhase analyzePhase actPhase\".split(\" \")\n getCategory(tags) {\n return tags.split(\" \").filter(w => w.endsWith(\"Phase\"))[0]\n }\n getNote(category) {\n return ` <span class=\"note\">A${category.replace(\"Phase\", \"\").substr(1)}Time.</span>`\n }\n get atomParsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"anyAtom\\n \").clone()\n const parserParticle = clone.getParticle(\"anyAtom\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n return parserParticle\n }\n get parsersToDocument() {\n const parsersParser = require(\"scrollsdk/products/parsers.nodejs.js\")\n const clone = new parsersParser(\"latinParser\\n \").clone()\n const parserParticle = clone.getParticle(\"latinParser\")\n const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)\n atoms.sort()\n parserParticle.setSubparticles(atoms.join(\"\\n\"))\n clone.appendLine(\"myParser\")\n clone.appendLine(\"myAtom\")\n return parserParticle\n }\nabstractMeasureParser\n atoms measureNameAtom\n cueFromId\n boolean isMeasure true\n float sortIndex 1.9\n boolean isComputed false\n string typeForWebForms text\n extends abstractScrollParser\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n return \"\"\n }\n get measureValue() {\n return this.content ?? \"\"\n }\n get measureName() {\n return this.getCuePath().replace(/ /g, \"_\")\n }\nabstractAtomMeasureParser\n description A measure that contains a single atom.\n atoms measureNameAtom atomAtom\n extends abstractMeasureParser\nabstractEmailMeasureParser\n string typeForWebForms email\n atoms measureNameAtom emailAddressAtom\n extends abstractAtomMeasureParser\nabstractUrlMeasureParser\n string typeForWebForms url\n atoms measureNameAtom urlAtom\n extends abstractAtomMeasureParser\nabstractStringMeasureParser\n catchAllAtomType stringAtom\n extends abstractMeasureParser\nabstractIdParser\n cue id\n description What is the ID of this concept?\n extends abstractStringMeasureParser\n float sortIndex 1\n boolean isMeasureRequired true\n boolean isConceptDelimiter true\n javascript\n getErrors() {\n const errors = super.getErrors()\n let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== \"id\")\n if (!requiredMeasureNames.length) return errors\n let next = this.next\n while (requiredMeasureNames.length && next.cue !== \"id\" && next.index !== 0) {\n requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)\n next = next.next\n }\n requiredMeasureNames.forEach(name =>\n errors.push(this.makeError(`Concept \"${this.content}\" is missing required measure \"${name}\".`))\n )\n return errors\n }\nabstractTextareaMeasureParser\n string typeForWebForms textarea\n extends abstractMeasureParser\n baseParser blobParser\n javascript\n get measureValue() {\n return this.subparticlesToString().replace(/\\n/g, \"\\\\n\")\n }\nabstractNumericMeasureParser\n string typeForWebForms number\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractIntegerMeasureParser\n atoms measureNameAtom integerAtom\n extends abstractNumericMeasureParser\nabstractFloatMeasureParser\n atoms measureNameAtom floatAtom\n extends abstractNumericMeasureParser\nabstractPercentageMeasureParser\n atoms measureNameAtom percentAtom\n extends abstractNumericMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : parseFloat(content)\n }\nabstractEnumMeasureParser\n atoms measureNameAtom enumAtom\n extends abstractMeasureParser\nabstractBooleanMeasureParser\n atoms measureNameAtom booleanAtom\n extends abstractMeasureParser\n javascript\n get measureValue() {\n const {content} = this\n return content === undefined ? \"\" : content == \"true\"\n }\nmetaTagsParser\n popularity 0.007693\n cueFromId\n extends abstractScrollParser\n description Print meta tags including title.\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\n buildHtml() {\n const {root} = this\n const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage } = root\n const rssFeedUrl = root.get(\"rssFeedUrl\")\n const favicon = root.get(\"favicon\")\n const faviconTag = favicon ? `<link rel=\"icon\" href=\"${favicon}\">` : \"\"\n const rssTag = rssFeedUrl ? `<link rel=\"alternate\" type=\"application/rss+xml\" title=\"${title}\" href=\"${rssFeedUrl}\">` : \"\"\n const gitTag = gitRepo ? `<link rel=\"source\" type=\"application/git\" title=\"Source Code Repository\" href=\"${gitRepo}\">` : \"\"\n return `<head>\n <meta charset=\"utf-8\">\n <title>${title}</title>\n <script>/* This HTML was generated by 📜 Scroll v${scrollVersion}. https://scroll.pub */</script>\n <style>@media print {.doNotPrint {display: none !important;}}</style>\n <link rel=\"canonical\" href=\"${canonicalUrl}\">\n <meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n <meta name=\"description\" content=\"${description}\">\n <meta name=\"generator\" content=\"Scroll v${scrollVersion}\">\n <meta property=\"og:title\" content=\"${title}\">\n <meta property=\"og:description\" content=\"${description}\">\n <meta property=\"og:image\" content=\"${openGraphImage}\">\n ${faviconTag}\n ${gitTag}\n ${rssTag}\n <meta name=\"twitter:card\" content=\"summary_large_image\">\n </head>\n <body>`\n }\nquoteParser\n popularity 0.001471\n cueFromId\n description A quote.\n catchAllParser quoteLineParser\n extends abstractScrollParser\n javascript\n buildHtml() {\n return `<blockquote class=\"scrollQuote\">${this.subparticlesToString()}</blockquote>`\n }\n buildTxt() {\n return this.subparticlesToString()\n }\nredirectToParser\n popularity 0.000072\n description HTML redirect tag.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cueFromId\n example\n redirectTo https://scroll.pub/releaseNotes.html\n javascript\n buildHtml() {\n return `<meta http-equiv=\"Refresh\" content=\"0; url='${this.getAtom(1)}'\" />`\n }\nabstractVariableParser\n extends abstractScrollParser\n catchAllAtomType stringAtom\n atoms preBuildCommandAtom\n cueFromId\n javascript\n isTopMatter = true\n buildHtml() {\n return \"\"\n }\nreplaceParser\n description Replace this with that.\n extends abstractVariableParser\n baseParser blobParser\n example\n replace YEAR 2022\nreplaceJsParser\n description Replace this with evaled JS.\n extends replaceParser\n catchAllAtomType javascriptAtom\n example\n replaceJs SUM 1+1\n * 1+1 = SUM\nreplaceNodejsParser\n description Replace with evaled Node.JS.\n extends abstractVariableParser\n catchAllAtomType javascriptAtom\n baseParser blobParser\n example\n replaceNodejs\n module.exports = {SCORE : 1 + 2}\n * The score is SCORE\nrunScriptParser\n popularity 0.000024\n description Run script and dump stdout.\n extends abstractScrollParser\n atoms cueAtom urlAtom\n cue run\n int filenameIndex 1\n javascript\n get dependencies() { return [this.filename]}\n results = \"Not yet run\"\n async execute() {\n if (!this.filename) return\n await this.root.fetch(this.filename)\n // todo: make async\n const { execSync } = require(\"child_process\")\n this.results = execSync(this.command)\n }\n get command() {\n const path = this.root.path\n const {filename }= this\n const fullPath = this.root.makeFullPath(filename)\n const ext = path.extname(filename).slice(1)\n const interpreterMap = {\n php: \"php\",\n py: \"python3\",\n rb: \"ruby\",\n pl: \"perl\",\n sh: \"sh\"\n }\n return [interpreterMap[ext], fullPath].join(\" \")\n }\n buildHtml() {\n return this.buildTxt()\n }\n get filename() {\n return this.getAtom(this.filenameIndex)\n }\n buildTxt() {\n return this.results.toString().trim()\n }\nquickRunScriptParser\n extends runScriptParser\n atoms urlAtom\n pattern ^[^\\s]+\\.(py|pl|sh|rb|php)[^\\s]*$\n int filenameIndex 0\nendSnippetParser\n popularity 0.004293\n description Cut for snippet here.\n extends abstractScrollParser\n cueFromId\n javascript\n buildHtml() {\n return \"\"\n }\ntoStampParser\n description Print a directory to stamp.\n extends abstractScrollParser\n catchAllAtomType filePathAtom\n cueFromId\n javascript\n buildTxt() {\n return this.makeStamp(this.root.makeFullPath(this.content))\n }\n buildHtml() {\n return `<pre>${this.buildTxt()}</pre>`\n }\n makeStamp(dir) {\n const fs = require('fs');\n const path = require('path');\n const { execSync } = require('child_process');\n let stamp = 'stamp\\n';\n const handleFile = (indentation, relativePath, itemPath, ) => {\n stamp += `${indentation}${relativePath}\\n`;\n const content = fs.readFileSync(itemPath, 'utf8');\n stamp += `${indentation} ${content.replace(/\\n/g, `\\n${indentation} `)}\\n`;\n }\n let gitTrackedFiles\n function processDirectory(currentPath, depth) {\n const items = fs.readdirSync(currentPath);\n items.forEach(item => {\n const itemPath = path.join(currentPath, item);\n const relativePath = path.relative(dir, itemPath);\n if (!gitTrackedFiles.has(item)) return\n const stats = fs.statSync(itemPath);\n const indentation = ' '.repeat(depth);\n if (stats.isDirectory()) {\n stamp += `${indentation}${relativePath}/\\n`;\n processDirectory(itemPath, depth + 1);\n } else if (stats.isFile())\n handleFile(indentation, relativePath, itemPath)\n });\n }\n const stats = fs.statSync(dir);\n if (stats.isDirectory()) {\n // Get list of git-tracked files\n gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })\n .split('\\n')\n .filter(Boolean))\n processDirectory(dir, 1)\n }\n else\n handleFile(\" \", dir, dir)\n return stamp.trim();\n }\nstampParser\n description Expand project template to disk.\n extends abstractScrollParser\n inScope stampFolderParser\n catchAllParser stampFileParser\n example\n stamp\n .gitignore\n *.html\n readme.scroll\n # Hello world\n <script src=\"scripts/nested/hello.js\"></script>\n scripts/\n nested/\n hello.js\n console.log(\"Hello world\")\n cueFromId\n atoms preBuildCommandAtom\n javascript\n execute() {\n const dir = this.root.folderPath\n this.forEach(particle => particle.execute(dir))\n }\nscrollStumpParser\n cue stump\n extends abstractScrollParser\n description Compile Stump to HTML.\n catchAllParser stumpContentParser\n javascript\n buildHtml() {\n const {stumpParser} = this\n return new stumpParser(this.subparticlesToString()).compile()\n }\n get stumpParser() {\n return this.isNodeJs() ? require(\"scrollsdk/products/stump.nodejs.js\") : stumpParser\n }\nstumpNoSnippetParser\n popularity 0.010177\n // todo: make noSnippets an aftertext directive?\n extends scrollStumpParser\n description Compile Stump unless snippet.\n cueFromId\n javascript\n buildHtmlSnippet() {\n return \"\"\n }\nplainTextParser\n description Plain text oneliner or block.\n cueFromId\n extends abstractScrollParser\n catchAllParser plainTextLineParser\n catchAllAtomType stringAtom\n javascript\n buildHtml() {\n return this.buildTxt()\n }\n buildTxt() {\n return `${this.content ?? \"\"}${this.subparticlesToString()}`\n }\nplainTextOnlyParser\n popularity 0.000072\n extends plainTextParser\n description Only print for buildTxt.\n javascript\n buildHtml() {\n return \"\"\n }\nscrollThemeParser\n popularity 0.007524\n boolean isPopular true\n cue theme\n extends abstractScrollParser\n catchAllAtomType scrollThemeAtom\n description A collection of simple themes.\n string copyFromExternal gazette.css\n // Note this will be replaced at runtime\n javascript\n get copyFromExternal() {\n return this.files.join(\" \")\n }\n get files() {\n return this.atoms.slice(1).map(name => `${name}.css`)\n }\n buildHtml() {\n return this.files.map(name => `<link rel=\"stylesheet\" type=\"text/css\" href=\"${name}\">`).join(\"\\n\")\n }\nabstractAftertextAttributeParser\n atoms cueAtom\n boolean isAttribute true\n javascript\n get htmlAttributes() {\n return `${this.cue}=\"${this.content}\"`\n }\n buildHtml() {\n return \"\"\n }\naftertextIdParser\n popularity 0.000145\n cue id\n description Provide an ID to be output in the generated HTML tag.\n extends abstractAftertextAttributeParser\n atoms cueAtom htmlIdAtom\n single\naftertextStyleParser\n popularity 0.000217\n cue style\n description Set HTML style attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType cssAnyAtom\n javascript\n htmlAttributes = \"\" // special case this one\n get css() { return `${this.property}:${this.content};` }\naftertextFontParser\n popularity 0.000217\n cue font\n description Set font.\n extends aftertextStyleParser\n atoms cueAtom fontFamilyAtom\n catchAllAtomType cssAnyAtom\n string property font-family\n javascript\n get css() {\n if (this.content === \"Slim\") return \"font-family:Helvetica Neue; font-weight:100;\"\n return super.css\n }\naftertextColorParser\n popularity 0.000217\n cue color\n description Set font color.\n extends aftertextStyleParser\n catchAllAtomType cssAnyAtom\n string property color\naftertextOnclickParser\n popularity 0.000217\n cue onclick\n description Set HTML onclick attribute.\n extends abstractAftertextAttributeParser\n catchAllAtomType anyAtom\naftertextHiddenParser\n cue hidden\n atoms cueAtom\n description Do not compile this particle to HTML.\n extends abstractAftertextAttributeParser\n single\naftertextTagParser\n atoms cueAtom htmlTagAtom\n description Override the HTML tag that the compiled particle will use.\n cue tag\n javascript\n buildHtml() {\n return \"\"\n }\nabstractAftertextDirectiveParser\n atoms cueAtom\n catchAllAtomType stringAtom\n javascript\n isMarkup = true\n buildHtml() {\n return \"\"\n }\n getErrors() {\n const errors = super.getErrors()\n if (!this.isMarkup || this.matchWholeLine) return errors\n const inserts = this.getInserts(this.parent.originalTextPostLinkify)\n // todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.\n // todo: also need to be able to map lines back to their line in source (pre-imports)\n if (!inserts.length)\n errors.push(this.makeError(`No match found for \"${this.getLine()}\".`))\n return errors\n }\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get shouldMatchAll() {\n return this.has(\"matchAll\")\n }\n getMatches(text) {\n const { pattern } = this\n const escapedPattern = pattern.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n return [...text.matchAll(new RegExp(escapedPattern, \"g\"))].map(match => {\n const { index } = match\n const endIndex = index + pattern.length\n return [\n { index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },\n { index: endIndex, endIndex, string: `</${this.closeTag}>` }\n ]\n })\n }\n getInserts(text) {\n const matches = this.getMatches(text)\n if (!matches.length) return false\n if (this.shouldMatchAll) return matches.flat()\n const match = this.getParticle(\"match\")\n if (match)\n return match.indexes\n .map(index => matches[index])\n .filter(i => i)\n .flat()\n return matches[0]\n }\n get allAttributes() {\n const attr = this.attributes.join(\" \")\n return attr ? \" \" + attr : \"\"\n }\n get attributes() {\n return []\n }\n get openTag() {\n return this.tag\n }\n get closeTag() {\n return this.tag\n }\nabstractMarkupParser\n extends abstractAftertextDirectiveParser\n inScope abstractMarkupParameterParser\n javascript\n get matchWholeLine() {\n return this.getAtomsFrom(this.patternStartsAtAtom).length === 0\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(\" \")\n }\n patternStartsAtAtom = 1\nboldParser\n popularity 0.000096\n cueFromId\n description Bold matching text.\n extends abstractMarkupParser\n javascript\n tag = \"b\"\nitalicsParser\n popularity 0.000241\n cueFromId\n description Italicize matching text.\n extends abstractMarkupParser\n javascript\n tag = \"i\"\nunderlineParser\n popularity 0.000024\n description Underline matching text.\n cueFromId\n extends abstractMarkupParser\n javascript\n tag = \"u\"\nafterTextCenterParser\n popularity 0.000193\n description Center paragraph.\n cue center\n extends abstractMarkupParser\n javascript\n tag = \"center\"\naftertextCodeParser\n popularity 0.000145\n description Wrap matching text in code span.\n cue code\n extends abstractMarkupParser\n javascript\n tag = \"code\"\naftertextStrikeParser\n popularity 0.000048\n description Wrap matching text in s span.\n cue strike\n extends abstractMarkupParser\n javascript\n tag = \"s\"\nclassMarkupParser\n popularity 0.000772\n description Add a custom class to the parent element instead. If matching text provided, a span with the class will be added around the matching text.\n extends abstractMarkupParser\n atoms cueAtom classNameAtom\n cue class\n javascript\n tag = \"span\"\n get applyToParentElement() {\n return this.atoms.length === 2\n }\n getInserts(text) {\n // If no select text is added, set the class on the parent element.\n if (this.applyToParentElement) return []\n return super.getInserts(text)\n }\n get className() {\n return this.getAtom(1)\n }\n get attributes() {\n return [`class=\"${this.className}\"`]\n }\n get matchWholeLine() {\n return this.applyToParentElement\n }\n get pattern() {\n return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(\" \")\n }\nclassesMarkupParser\n extends classMarkupParser\n cue classes\n javascript\n applyToParentElement = true\n get className() {\n return this.content\n }\nhoverNoteParser\n popularity 0.000265\n description Add a caveat viewable on hover on matching text. When you want to be sure you've thoroughly addressed obvious concerns but ones that don't warrant to distract from the main argument of the text.\n cueFromId\n extends classMarkupParser\n catchAllParser lineOfTextParser\n atoms cueAtom\n javascript\n get pattern() {\n return this.getAtomsFrom(1).join(\" \")\n }\n get attributes() {\n return [`class=\"scrollHoverNote\"`, `title=\"${this.hoverNoteText}\"`]\n }\n get hoverNoteText() {\n return this.subparticlesToString().replace(/\\n/g, \" \")\n }\nlinkParser\n popularity 0.008706\n extends abstractMarkupParser\n description Put the matching text in an <a> tag.\n atoms cueAtom urlAtom\n inScope linkTitleParser linkTargetParser abstractCommentParser\n programParser\n description Anything here will be URI encoded and then appended to the link.\n cueFromId\n atoms cueAtom\n catchAllParser programLinkParser\n javascript\n get encoded() {\n return encodeURIComponent(this.subparticlesToString())\n }\n cueFromId\n javascript\n tag = \"a\"\n buildTxt() {\n return this.root.ensureAbsoluteLink(this.link) + \" \" + this.pattern\n }\n get link() {\n const {baseLink} = this\n if (this.has(\"program\"))\n return baseLink + this.getParticle(\"program\").encoded\n return baseLink\n }\n get baseLink() {\n const link = this.getAtom(1)\n const isAbsoluteLink = link.includes(\"://\")\n if (isAbsoluteLink) return link\n const relativePath = this.parent.buildSettings?.relativePath || \"\"\n return relativePath + link\n }\n get attributes() {\n const attrs = [`href=\"${this.link}\"`]\n const options = [\"title\", \"target\"]\n options.forEach(option => {\n const particle = this.getParticle(option)\n if (particle) attrs.push(`${option}=\"${particle.content}\"`)\n })\n return attrs\n }\n patternStartsAtAtom = 2\nemailLinkParser\n popularity 0.000048\n description A mailto link\n cue email\n extends linkParser\n javascript\n get attributes() {\n return [`href=\"mailto:${this.link}\"`]\n }\nquickLinkParser\n popularity 0.029228\n pattern ^https?\\:\n extends linkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\nquickRelativeLinkParser\n popularity 0.029228\n description Relative links.\n // note: only works if relative link ends in .html\n pattern ^[^\\s]+\\.(html|htm)\n extends linkParser\n atoms urlAtom\n javascript\n get link() {\n return this.cue\n }\n patternStartsAtAtom = 1\ndatelineParser\n popularity 0.006005\n cueFromId\n description Gives your paragraph a dateline like \"December 15, 2021 — The...\"\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const {day} = this\n if (!day) return false\n return [{ index: 0, string: `<span class=\"scrollDateline\">${day} — </span>` }]\n }\n matchWholeLine = true\n get day() {\n let day = this.content || this.root.date\n if (!day) return \"\"\n return this.root.dayjs(day).format(`MMMM D, YYYY`)\n }\ndayjsParser\n description Advanced directive that evals some Javascript code in an environment including \"dayjs\".\n cueFromId\n extends abstractAftertextDirectiveParser\n javascript\n getInserts() {\n const dayjs = this.root.dayjs\n const days = eval(this.content)\n const index = this.parent.originalTextPostLinkify.indexOf(\"days\")\n return [{ index, string: `${days} ` }]\n }\ninlineMarkupsOnParser\n popularity 0.000024\n cueFromId\n description Enable these inline markups only.\n example\n Hello *world*!\n inlineMarkupsOn bold\n extends abstractAftertextDirectiveParser\n catchAllAtomType inlineMarkupNameAtom\n javascript\n get shouldMatchAll() {\n return true\n }\n get markups() {\n const {root} = this\n let markups = [{delimiter: \"`\", tag: \"code\", exclusive: true, name: \"code\"},{delimiter: \"*\", tag: \"strong\", name: \"bold\"}, {delimiter: \"_\", tag: \"em\", name: \"italics\"}]\n // only add katex markup if the root doc has katex.\n if (root.has(\"katex\"))\n markups.unshift({delimiter: \"$\", tag: \"span\", attributes: ' class=\"scrollKatex\"', exclusive: true, name: \"katex\"})\n if (this.content)\n return markups.filter(markup => this.content.includes(markup.name))\n if (root.has(\"inlineMarkups\")) {\n root.getParticle(\"inlineMarkups\").forEach(markup => {\n const delimiter = markup.getAtom(0)\n const tag = markup.getAtom(1)\n // todo: add support for providing custom functions for inline markups?\n // for example, !2+2! could run eval, or :about: could search a link map.\n const attributes = markup.getAtomsFrom(2).join(\" \")\n markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups\n if (tag)\n markups.push({delimiter, tag, attributes})\n })\n }\n return markups\n }\n matchWholeLine = true\n getMatches(text) {\n const exclusives = []\n return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()\n }\n applyMarkup(text, markup, exclusives = []) {\n const {delimiter, tag, attributes} = markup\n const escapedDelimiter = delimiter.replace(/[-\\/\\\\^$*+?.()|[\\]{}]/g, \"\\\\$&\")\n const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, \"g\")\n const delimiterLength = delimiter.length\n return [...text.matchAll(pattern)].map(match => {\n const { index } = match\n const endIndex = index + match[0].length\n // I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.\n // The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.\n // Note that the way this is currently implemented any TeX in an inline code will get rendered, but code\n // inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.\n if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))\n return undefined\n if (markup.exclusive)\n exclusives.push([index, endIndex])\n return [\n { index, string: `<${tag + (attributes ? \" \" + attributes : \"\")}>`, endIndex, consumeStartCharacters: delimiterLength },\n { index: endIndex, endIndex, string: `</${tag}>`, consumeEndCharacters: delimiterLength }\n ]\n }).filter(i => i)\n }\ninlineMarkupParser\n popularity 0.000169\n cueFromId\n atoms cueAtom delimiterAtom tagOrUrlAtom\n catchAllAtomType htmlAttributesAtom\n extends inlineMarkupsOnParser\n description Custom inline markup. for\n example\n @This@ will be in italics.\n inlineMarkup @ em\n javascript\n getMatches(text) {\n try {\n const delimiter = this.getAtom(1)\n const tag = this.getAtom(2)\n const attributes = this.getAtomsFrom(3).join(\" \")\n return this.applyMarkup(text, {delimiter, tag, attributes})\n } catch (err) {\n console.error(err)\n return []\n }\n // Note: doubling up doesn't work because of the consumption characters.\n }\nlinkifyParser\n description Use this to disable linkify on the text.\n extends abstractAftertextDirectiveParser\n cueFromId\n atoms cueAtom booleanAtom\nabstractMarkupParameterParser\n atoms cueAtom\n cueFromId\nmatchAllParser\n popularity 0.000024\n description Use this to match all occurrences of the text.\n extends abstractMarkupParameterParser\nmatchParser\n popularity 0.000048\n catchAllAtomType integerAtom\n description Use this to specify which index(es) to match.\n javascript\n get indexes() {\n return this.getAtomsFrom(1).map(num => parseInt(num))\n }\n example\n aftertext\n hello ello ello\n bold ello\n match 0 2\n extends abstractMarkupParameterParser\nabstractHtmlAttributeParser\n javascript\n buildHtml() {\n return \"\"\n }\nlinkTargetParser\n popularity 0.000024\n extends abstractHtmlAttributeParser\n description If you want to set the target of the link. To \"_blank\", for example.\n cue target\n atoms cueAtom anyAtom\nblankLineParser\n popularity 0.308149\n description Print nothing. Break section.\n atoms blankAtom\n boolean isPopular true\n javascript\n buildHtml() {\n return this.parent.clearSectionStack()\n }\n pattern ^$\n tags doNotSynthesize\nchatLineParser\n popularity 0.009887\n catchAllAtomType anyAtom\n catchAllParser chatLineParser\nlineOfCodeParser\n popularity 0.018665\n catchAllAtomType codeAtom\n catchAllParser lineOfCodeParser\ncommentLineParser\n catchAllAtomType commentAtom\ncssLineParser\n popularity 0.002870\n catchAllAtomType cssAnyAtom\n catchAllParser cssLineParser\nabstractTableTransformParser\n atoms cueAtom\n inScope abstractTableVisualizationParser abstractTableTransformParser h1Parser h2Parser scrollQuestionParser htmlInlineParser scrollBrParser\n javascript\n get coreTable() {\n return this.parent.coreTable\n }\n get columnNames() {\n return this.parent.columnNames\n }\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\n getRunTimeEnumOptionsForValidation(atom) {\n // Note: this will fail if the CSV file hasnt been built yet.\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames.concat(this.parent.columnNames.map(c => \"-\" + c)) // Add reverse names\n return super.getRunTimeEnumOptions(atom)\n }\nabstractDateSplitTransformParser\n extends abstractTableTransformParser\n atoms cueAtom\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const columnName = this.getAtom(1) || this.detectDateColumn()\n if (!columnName) return this.parent.coreTable\n return this.parent.coreTable.map(row => {\n const newRow = {...row}\n try {\n const date = this.root.dayjs(row[columnName])\n if (date.isValid())\n newRow[this.newColumnName] = this.transformDate(date)\n } catch (err) {}\n return newRow\n })\n }\n detectDateColumn() {\n const columns = this.parent.columnNames\n const dateColumns = ['date', 'created', 'published', 'timestamp']\n for (const col of dateColumns) {\n if (columns.includes(col)) return col\n }\n for (const col of columns) {\n const sample = this.parent.coreTable[0][col]\n if (sample && this.root.dayjs(sample).isValid())\n return col\n }\n return null\n }\n get columnNames() {\n return [...this.parent.columnNames, this.newColumnName]\n }\n transformDate(date) {\n const formatted = date.format(this.dateFormat)\n const isInt = !this.cue.includes(\"Name\")\n return isInt ? parseInt(formatted) : formatted\n }\nscrollSplitYearParser\n extends abstractDateSplitTransformParser\n description Extract year into new column.\n cue splitYear\n string newColumnName year\n string dateFormat YYYY\nscrollSplitDayNameParser\n extends abstractDateSplitTransformParser\n description Extract day name into new column.\n cue splitDayName\n string newColumnName dayName\n string dateFormat dddd\nscrollSplitMonthNameParser\n extends abstractDateSplitTransformParser\n description Extract month name into new column.\n cue splitMonthName\n string newColumnName monthName\n string dateFormat MMMM\nscrollSplitMonthParser\n extends abstractDateSplitTransformParser\n description Extract month number (1-12) into new column.\n cue splitMonth\n string newColumnName month\n string dateFormat M\nscrollSplitDayOfMonthParser\n extends abstractDateSplitTransformParser\n description Extract day of month (1-31) into new column.\n cue splitDayOfMonth\n string newColumnName dayOfMonth\n string dateFormat D\nscrollSplitDayOfWeekParser\n extends abstractDateSplitTransformParser\n description Extract day of week (0-6) into new column.\n cue splitDay\n string newColumnName day\n string dateFormat d\nscrollGroupByParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Combine rows with matching values into groups.\n example\n tables posts.csv\n groupBy year\n printTable\n cue groupBy\n javascript\n get coreTable() {\n if (this._coreTable) return this._coreTable\n const groupByColNames = this.getAtomsFrom(1)\n const {coreTable} = this.parent\n if (!groupByColNames.length) return coreTable\n const newCols = this.findParticles(\"reduce\").map(reduceParticle => {\n return {\n source: reduceParticle.getAtom(1),\n reduction: reduceParticle.getAtom(2),\n name: reduceParticle.getAtom(3) || reduceParticle.getAtomsFrom(1).join(\"_\")\n }\n })\n // Pivot is shorthand for group and reduce?\n const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {\n const colMap = {}\n inputColumnNames.forEach((col) => (colMap[col] = true))\n const groupByCols = groupByColumnNames.filter((col) => colMap[col])\n return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)\n }\n class PivotTable {\n constructor(rows, inputColumns, outputColumns) {\n this._columns = {}\n this._rows = rows\n inputColumns.forEach((col) => (this._columns[col.name] = col))\n outputColumns.forEach((col) => (this._columns[col.name] = col))\n }\n _getGroups(allRows, groupByColNames) {\n const rowsInGroups = new Map()\n allRows.forEach((row) => {\n const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, \"\") || \"\").join(\" \")\n if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])\n rowsInGroups.get(groupKey).push(row)\n })\n return rowsInGroups\n }\n getNewRows(groupByCols) {\n // make new particles\n const rowsInGroups = this._getGroups(this._rows, groupByCols)\n // Any column in the group should be reused by the children\n const columns = [\n {\n name: \"count\",\n type: \"number\",\n min: 0,\n },\n ]\n groupByCols.forEach((colName) => columns.push(this._columns[colName]))\n const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)\n colsToReduce.forEach((col) => columns.push(col))\n // for each group\n const rows = []\n const totalGroups = rowsInGroups.size\n for (let [groupId, group] of rowsInGroups) {\n const firstRow = group[0]\n const newRow = {}\n groupByCols.forEach((col) =>\n newRow[col] = firstRow ? firstRow[col] : 0\n )\n newRow.count = group.length\n // todo: add more reductions? count, stddev, median, variance.\n colsToReduce.forEach((col) => {\n const sourceColName = col.source\n const reduction = col.reduction\n if (reduction === \"concat\") {\n newRow[col.name] = group.map((row) => row[sourceColName]).join(\" \")\n return \n }\n if (reduction === \"first\") {\n newRow[col.name] = group[0][sourceColName]\n return \n }\n const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === \"number\" && !isNaN(val))\n let reducedValue = firstRow[sourceColName]\n if (reduction === \"sum\") reducedValue = values.reduce((prev, current) => prev + current, 0)\n if (reduction === \"max\") reducedValue = Math.max(...values)\n if (reduction === \"min\") reducedValue = Math.min(...values)\n if (reduction === \"mean\") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length\n newRow[col.name] = reducedValue\n })\n rows.push(newRow)\n }\n // todo: add tests. figure out this api better.\n Object.values(columns).forEach((col) => {\n // For pivot columns, remove the source and reduction info for now. Treat things as immutable.\n delete col.source\n delete col.reduction\n })\n return {\n rows,\n columns,\n }\n }\n }\n const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)\n this._coreTable = pivotTable.rows\n this._columnNames = pivotTable.columns.map(col => col.name)\n return pivotTable.rows\n }\n get columnNames() {\n const {coreTable} = this\n return this._columnNames || this.parent.columnNames\n }\nscrollWhereParser\n extends abstractTableTransformParser\n description Filter rows by condition.\n cue where\n atoms cueAtom columnNameAtom comparisonAtom atomAtom\n example\n table iris.csv\n where Species = setosa\n javascript\n get coreTable() {\n // todo: use atoms here.\n const columnName = this.getAtom(1)\n const operator = this.getAtom(2)\n let untypedScalarValue = this.getAtom(3)\n const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)\n const coreTable = this.parent.coreTable\n if (!columnName || !operator || untypedScalarValue === undefined) return coreTable\n const filterFn = row => {\n const atom = row[columnName]\n const typedAtom = atom === null ? undefined : atom // convert nulls to undefined\n if (operator === \"=\") return typedValue === typedAtom\n else if (operator === \"!=\") return typedValue !== typedAtom\n else if (operator === \"includes\") return typedAtom !== undefined && typedAtom.includes(typedValue)\n else if (operator === \"startsWith\") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)\n else if (operator === \"endsWith\") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)\n else if (operator === \"doesNotInclude\") return typedAtom === undefined || !typedAtom.includes(typedValue)\n else if (operator === \">\") return typedAtom > typedValue\n else if (operator === \"<\") return typedAtom < typedValue\n else if (operator === \">=\") return typedAtom >= typedValue\n else if (operator === \"<=\") return typedAtom <= typedValue\n else if (operator === \"empty\") return atom === \"\" || atom === undefined\n else if (operator === \"notEmpty\") return !(atom === \"\" || atom === undefined)\n }\n return coreTable.filter(filterFn)\n }\nscrollSelectParser\n catchAllAtomType columnNameAtom\n extends abstractTableTransformParser\n description Drop all columns except these.\n example\n tables\n data\n name,year,count\n index,2022,2\n about,2023,4\n select name year\n printTable\n cue select\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {columnNames} = this\n if (!columnNames.length) return coreTable\n return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))\n }\n get columnNames() {\n return this.getAtomsFrom(1)\n }\nscrollReverseParser\n extends abstractTableTransformParser\n description Reverse rows.\n cue reverse\n javascript\n get coreTable() {\n return this.parent.coreTable.slice().reverse()\n }\nscrollComposeParser\n extends abstractTableTransformParser\n description Add column using format string.\n catchAllAtomType stringAtom\n cue compose\n example\n table\n compose My name is {name}\n printTable\n javascript\n get coreTable() {\n const {newColumnName} = this\n const formatString = this.getAtomsFrom(2).join(\" \")\n return this.parent.coreTable.map((row, index) => {\n const newRow = Object.assign({}, row)\n newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)\n return newRow\n })\n }\n evaluate(str) {\n return str\n }\n get newColumnName() {\n return this.atoms[1]\n }\n get columnNames() {\n return this.parent.columnNames.concat(this.newColumnName)\n }\nscrollComputeParser\n extends scrollComposeParser\n description Add column by evaling format string.\n catchAllAtomType stringAtom\n cue compute\n javascript\n evaluate(str) {\n return parseFloat(eval(str))\n }\nscrollEvalParser\n extends scrollComputeParser\n description Add column by evaling format string.\n cue eval\n javascript\n evaluate(str) {\n return eval(str)\n }\nscrollRankParser\n extends scrollComposeParser\n description Add rank column.\n string newColumnName rank\n cue rank\n javascript\n evaluate(str, index) { return index + 1 }\nscrollLinksParser\n extends abstractTableTransformParser\n description Add column with links.\n cue links\n catchAllAtomType columnNameAtom\n javascript\n get coreTable() {\n const {newColumnName, linkColumns} = this\n return this.parent.coreTable.map(row => {\n const newRow = Object.assign({}, row)\n let newValue = []\n linkColumns.forEach(name => {\n const value = newRow[name]\n delete newRow[name]\n if (value) newValue.push(`<a href=\"${value.includes(\"@\") ? \"mailto:\" : \"\"}${value}\">${name}</a>`)\n })\n newRow[newColumnName] = newValue.join(\" \")\n return newRow\n })\n }\n get newColumnName() {\n return \"links\"\n }\n get linkColumns() {\n return this.getAtomsFrom(1)\n }\n get columnNames() {\n const {linkColumns} = this\n return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)\n }\nscrollLimitParser\n extends abstractTableTransformParser\n description Select a subset.\n cue limit\n atoms cueAtom integerAtom integerAtom\n javascript\n get coreTable() {\n let start = this.getAtom(1)\n let end = this.getAtom(2)\n if (end === undefined) {\n end = start\n start = 0\n }\n return this.parent.coreTable.slice(parseInt(start), parseInt(end))\n }\nscrollShuffleParser\n extends abstractTableTransformParser\n description Randomly reorder rows.\n cue shuffle\n example\n table data.csv\n shuffle\n printTable\n javascript\n get coreTable() {\n // Create a copy of the table to avoid modifying original\n const rows = this.parent.coreTable.slice()\n // Fisher-Yates shuffle algorithm\n for (let i = rows.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1))\n ;[rows[i], rows[j]] = [rows[j], rows[i]]\n }\n return rows\n }\nscrollTransposeParser\n extends abstractTableTransformParser\n description Tranpose table.\n cue transpose\n javascript\n get coreTable() {\n // todo: we need to switch to column based coreTable, instead of row based\n const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);\n return transpose(this.parent.coreTable)\n }\nscrollImputeParser\n extends abstractTableTransformParser\n description Impute missing values of a columm.\n atoms cueAtom columnNameAtom\n cue impute\n javascript\n get coreTable() {\n const {columnName} = this\n const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)\n // ascending\n const imputed = []\n let lastInserted = sorted[0][columnName]\n sorted.forEach(row => {\n const measuredTime = row[columnName]\n while (measuredTime > lastInserted + 1) {\n lastInserted++\n // synthesize rows\n const imputedRow = {}\n imputedRow[columnName] = lastInserted\n imputedRow.count = 0\n imputed.push(imputedRow)\n }\n lastInserted = measuredTime\n imputed.push(row)\n })\n return imputed\n }\n get columnName() {\n return this.getAtom(1)\n }\nscrollOrderByParser\n extends abstractTableTransformParser\n description Sort rows by column(s).\n catchAllAtomType columnNameAtom\n cue orderBy\n javascript\n get coreTable() {\n const makeLodashOrderByParams = str => {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n const orderBy = makeLodashOrderByParams(this.content)\n return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])\n }\nscrollRenameParser\n // todo: add support in Parsers for tuple catch alls\n catchAllAtomType columnNameAtom atomAtom\n catchAllAtomType atomAtom\n extends abstractTableTransformParser\n description Rename columns.\n example\n tables\n data\n name,year,count\n index,2022,2\n rename name Name year Year\n printTable\n cue rename\n javascript\n get coreTable() {\n const {coreTable} = this.parent\n const {renameMap} = this\n if (!Object.keys(renameMap).length) return coreTable\n return coreTable.map(row => {\n const newRow = {}\n Object.keys(row).forEach(key => {\n const name = renameMap[key] || key\n newRow[name] = row[key]\n })\n return newRow\n })\n }\n get renameMap() {\n const map = {}\n const pairs = this.getAtomsFrom(1)\n let oldName\n while (oldName = pairs.shift()) {\n map[oldName] = pairs.shift()\n }\n return map\n }\n _renamed\n get columnNames() {\n if (this._renamed)\n return this._renamed\n const {renameMap} = this\n this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )\n return this._renamed\n }\nerrorParser\n baseParser errorParser\nhakonContentParser\n popularity 0.102322\n catchAllAtomType anyAtom\nheatrixCatchAllParser\n popularity 0.000193\n // todo Fill this out\n catchAllAtomType stringAtom\nlineOfTextParser\n popularity 0.000289\n catchAllAtomType stringAtom\n boolean isTextParser true\nhtmlLineParser\n popularity 0.005209\n catchAllAtomType htmlAnyAtom\n catchAllParser htmlLineParser\nopenGraphParser\n // todo: fix Parsers scope issue so we can move this parser def under scrollImageParser\n description Add this line to make this the open graph image.\n cueFromId\n atoms cueAtom\nscrollFooterParser\n description Import to bottom of file.\n atoms preBuildCommandAtom\n cue footer\nscriptLineParser\n catchAllAtomType scriptAnyAtom\n catchAllParser scriptLineParser\nlinkTitleParser\n popularity 0.000048\n description If you want to set the title of the link.\n cue title\n atoms cueAtom\n catchAllAtomType anyAtom\n example\n * This report showed the treatment had a big impact.\n https://example.com/report This report.\n title The average growth in the treatment group was 14.2x higher than the control group.\nprogramLinkParser\n popularity 0.000531\n catchAllAtomType codeAtom\nscrollMediaLoopParser\n popularity 0.000048\n cue loop\n atoms cueAtom\nscrollAutoplayParser\n cue autoplay\n atoms cueAtom\nabstractCompilerRuleParser\n catchAllAtomType anyAtom\n atoms cueAtom\ncloseSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is appended to the compiled and joined subparticles. Default is blank.\n cueFromId\nindentCharacterParser\n extends abstractCompilerRuleParser\n description You can change the indent character for compiled subparticles. Default is a space.\n cueFromId\ncatchAllAtomDelimiterParser\n description If a particle has a catchAllAtom, this is the string delimiter that will be used to join those atoms. Default is comma.\n extends abstractCompilerRuleParser\n cueFromId\nopenSubparticlesParser\n extends abstractCompilerRuleParser\n description When compiling a parent particle to a string, this string is prepended to the compiled and joined subparticles. Default is blank.\n cueFromId\nstringTemplateParser\n extends abstractCompilerRuleParser\n description This template string is used to compile this line, and accepts strings of the format: const var = {someAtomId}\n cueFromId\njoinSubparticlesWithParser\n description When compiling a parent particle to a string, subparticles are compiled to strings and joined by this character. Default is a newline.\n extends abstractCompilerRuleParser\n cueFromId\nabstractConstantParser\n description A constant.\n atoms cueAtom\n cueFromId\n // todo: make tags inherit\n tags actPhase\nparsersBooleanParser\n cue boolean\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType booleanAtom\n extends abstractConstantParser\n tags actPhase\nparsersFloatParser\n cue float\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType floatAtom\n extends abstractConstantParser\n tags actPhase\nparsersIntParser\n cue int\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType integerAtom\n tags actPhase\n extends abstractConstantParser\nparsersStringParser\n cue string\n atoms cueAtom constantIdentifierAtom\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n extends abstractConstantParser\n tags actPhase\nabstractParserRuleParser\n single\n atoms cueAtom\nabstractNonTerminalParserRuleParser\n extends abstractParserRuleParser\nparsersBaseParserParser\n atoms cueAtom baseParsersAtom\n description Set for blobs or errors. \n // In rare cases with untyped content you can use a blobParser, for now, to skip parsing for performance gains. The base errorParser will report errors when parsed. Use that if you don't want to implement your own error parser.\n extends abstractParserRuleParser\n cue baseParser\n tags analyzePhase\ncatchAllAtomTypeParser\n atoms cueAtom atomTypeIdAtom\n description Use for lists.\n // Aka 'listAtomType'. Use this when the value in a key/value pair is a list. If there are extra atoms in the particle's line, parse these atoms as this type. Often used with `listDelimiterParser`.\n extends abstractParserRuleParser\n cueFromId\n tags analyzePhase\natomParserParser\n atoms cueAtom atomParserAtom\n description Set parsing strategy.\n // prefix/postfix/omnifix parsing strategy. If missing, defaults to prefix.\n extends abstractParserRuleParser\n cueFromId\n tags experimental analyzePhase\ncatchAllParserParser\n description Attach this to unmatched lines.\n // If a parser is not found in the inScope list, instantiate this type of particle instead.\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersAtomsParser\n catchAllAtomType atomTypeIdAtom\n description Set required atomTypes.\n extends abstractParserRuleParser\n cue atoms\n tags analyzePhase\nparsersCompilerParser\n // todo Remove this and its subparticles?\n description Deprecated. For simple compilers.\n inScope stringTemplateParser catchAllAtomDelimiterParser openSubparticlesParser closeSubparticlesParser indentCharacterParser joinSubparticlesWithParser\n extends abstractParserRuleParser\n cue compiler\n tags deprecate\n boolean suggestInAutocomplete false\nparserDescriptionParser\n description Parser description.\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n cue description\n tags assemblePhase\nparsersExampleParser\n // todo Should this just be a \"string\" constant on particles?\n description Set example for docs and tests.\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n extends abstractParserRuleParser\n cue example\n tags assemblePhase\nextendsParserParser\n cue extends\n tags assemblePhase\n description Extend another parser.\n // todo: add a catchall that is used for mixins\n atoms cueAtom parserIdAtom\n extends abstractParserRuleParser\nparsersPopularityParser\n // todo Remove this parser. Switch to conditional frequencies.\n description Parser popularity.\n atoms cueAtom floatAtom\n extends abstractParserRuleParser\n cue popularity\n tags assemblePhase\ninScopeParser\n description Parsers in scope.\n catchAllAtomType parserIdAtom\n extends abstractParserRuleParser\n cueFromId\n tags acquirePhase\nparsersJavascriptParser\n // todo Urgently need to get submode syntax highlighting running! (And eventually LSP)\n description Javascript code for Parser Actions.\n catchAllParser catchAllJavascriptCodeLineParser\n extends abstractParserRuleParser\n tags actPhase\n javascript\n format() {\n if (this.isNodeJs()) {\n const template = `class FOO{ ${this.subparticlesToString()}}`\n this.setSubparticles(\n require(\"prettier\")\n .format(template, { semi: false, useTabs: true, parser: \"babel\", printWidth: 240 })\n .replace(/class FOO \\{\\s+/, \"\")\n .replace(/\\s+\\}\\s+$/, \"\")\n .replace(/\\n\\t/g, \"\\n\") // drop one level of indent\n .replace(/\\t/g, \" \") // we used tabs instead of spaces to be able to dedent without breaking literals.\n )\n }\n return this\n }\n cue javascript\nabstractParseRuleParser\n // Each particle should have a pattern that it matches on unless it's a catch all particle.\n extends abstractParserRuleParser\n cueFromId\nparsersCueParser\n atoms cueAtom stringAtom\n description Attach by matching first atom.\n extends abstractParseRuleParser\n tags acquirePhase\n cue cue\ncueFromIdParser\n atoms cueAtom\n description Derive cue from parserId.\n // for example 'fooParser' would have cue of 'foo'.\n extends abstractParseRuleParser\n tags acquirePhase\nparsersPatternParser\n catchAllAtomType regexAtom\n description Attach via regex.\n extends abstractParseRuleParser\n tags acquirePhase\n cue pattern\nparsersRequiredParser\n description Assert is present at least once.\n extends abstractParserRuleParser\n cue required\n tags analyzePhase\nabstractValidationRuleParser\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType booleanAtom\nparsersSingleParser\n description Assert used once.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\n cue single\nuniqueLineParser\n description Assert unique lines. For pattern parsers.\n // Can be overridden by a child class by setting to false.\n extends abstractValidationRuleParser\n tags analyzePhase\nuniqueCueParser\n description Assert unique first atoms. For pattern parsers.\n // For catch all parsers or pattern particles, use this to indicate the \n extends abstractValidationRuleParser\n tags analyzePhase\nlistDelimiterParser\n description Split content by this delimiter.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags analyzePhase\ncontentKeyParser\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with isomorphic JSON serialization/deserialization. If present will serialize the particle to an object and set a property with this key and the value set to the particle's content.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nsubparticlesKeyParser\n // todo: deprecate?\n description Deprecated. For to/from JSON.\n // Advanced keyword to help with serialization/deserialization of blobs. If present will serialize the particle to an object and set a property with this key and the value set to the particle's subparticles.\n extends abstractParserRuleParser\n cueFromId\n catchAllAtomType stringAtom\n tags deprecate\n boolean suggestInAutocomplete false\nparsersTagsParser\n catchAllAtomType stringAtom\n extends abstractParserRuleParser\n description Custom metadata.\n cue tags\n tags assemblePhase\natomTypeDescriptionParser\n description Atom Type description.\n catchAllAtomType stringAtom\n cue description\n tags assemblePhase\ncatchAllErrorParser\n baseParser errorParser\ncatchAllExampleLineParser\n catchAllAtomType exampleAnyAtom\n catchAllParser catchAllExampleLineParser\n atoms exampleAnyAtom\ncatchAllJavascriptCodeLineParser\n catchAllAtomType javascriptCodeAtom\n catchAllParser catchAllJavascriptCodeLineParser\ncatchAllMultilineStringConstantParser\n description String constants can span multiple lines.\n catchAllAtomType stringAtom\n catchAllParser catchAllMultilineStringConstantParser\n atoms stringAtom\natomTypeDefinitionParser\n // todo Generate a class for each atom type?\n // todo Allow abstract atom types?\n // todo Change pattern to postfix.\n pattern ^[a-zA-Z0-9_]+Atom$\n inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser\n atoms atomTypeIdAtom\n tags assemblePhase\n javascript\n buildHtml() {return \"\"}\nenumFromAtomTypesParser\n description Runtime enum options.\n catchAllAtomType atomTypeIdAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nparsersEnumParser\n description Set enum options.\n cue enum\n catchAllAtomType enumOptionAtom\n atoms atomPropertyNameAtom\n tags analyzePhase\nparsersExamplesParser\n description Examples for documentation and tests.\n // If the domain of possible atom values is large, such as a string type, it can help certain methods—such as program synthesis—to provide a few examples.\n cue examples\n catchAllAtomType atomExampleAtom\n atoms atomPropertyNameAtom\n tags assemblePhase\natomMinParser\n description Specify a min if numeric.\n cue min\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\natomMaxParser\n description Specify a max if numeric.\n cue max\n atoms atomPropertyNameAtom numberAtom\n tags analyzePhase\nparsersPaintParser\n atoms cueAtom paintTypeAtom\n description Instructor editor how to color these.\n single\n cue paint\n tags analyzePhase\nparserDefinitionParser\n // todo Add multiple dispatch?\n pattern ^[a-zA-Z0-9_]+Parser$\n description Parser types are a core unit of your language. They translate to 1 class per parser. Examples of parser would be \"header\", \"person\", \"if\", \"+\", \"define\", etc.\n catchAllParser catchAllErrorParser\n inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser\n atoms parserIdAtom\n tags assemblePhase\n javascript\n buildHtml() { return \"\"}\nparsersRegexParser\n catchAllAtomType regexAtom\n description Atoms must match this.\n single\n atoms atomPropertyNameAtom\n cue regex\n tags analyzePhase\nreservedAtomsParser\n single\n description Atoms can't be any of these.\n catchAllAtomType reservedAtomAtom\n atoms atomPropertyNameAtom\n cueFromId\n tags analyzePhase\nextendsAtomTypeParser\n cue extends\n description Extend another atomType.\n // todo Add mixin support in addition to extends?\n atoms cueAtom atomTypeIdAtom\n tags assemblePhase\n single\nabstractColumnNameParser\n atoms cueAtom columnNameAtom\n javascript\n getRunTimeEnumOptions(atom) {\n if (atom.atomTypeId === \"columnNameAtom\")\n return this.parent.columnNames\n return super.getRunTimeEnumOptions(atom)\n }\nscrollRadiusParser\n cue radius\n extends abstractColumnNameParser\nscrollSymbolParser\n cue symbol\n extends abstractColumnNameParser\nscrollFillParser\n cue fill\n extends abstractColumnNameParser\nscrollLabelParser\n cue label\n extends abstractColumnNameParser\nscrollXParser\n cue x\n extends abstractColumnNameParser\nscrollYParser\n cue y\n extends abstractColumnNameParser\nquoteLineParser\n popularity 0.004172\n catchAllAtomType anyAtom\n catchAllParser quoteLineParser\nscrollParser\n description Scroll is a language for scientists of all ages. Refine, share and collaborate on ideas.\n root\n inScope abstractScrollParser blankLineParser atomTypeDefinitionParser parserDefinitionParser\n catchAllParser catchAllParagraphParser\n javascript\n setFile(file) {\n this.file = file\n const date = this.get(\"date\")\n if (date) this.file.timestamp = this.dayjs(this.get(\"date\")).unix()\n return this\n }\n buildHtml(buildSettings) {\n this.sectionStack = []\n return this.filter(subparticle => subparticle.buildHtml).map(subparticle => { try {return subparticle.buildHtml(buildSettings)} catch (err) {console.error(err); return \"\"} }).filter(i => i).join(\"\\n\") + this.clearSectionStack()\n }\n sectionStack = []\n clearSectionStack() {\n const result = this.sectionStack.join(\"\\n\")\n this.sectionStack = []\n return result\n }\n bodyStack = []\n clearBodyStack() {\n const result = this.bodyStack.join(\"\")\n this.bodyStack = []\n return result\n }\n get hakonParser() {\n if (this.isNodeJs())\n return require(\"scrollsdk/products/hakon.nodejs.js\")\n return hakonParser\n }\n readSyncFromFileOrUrl(fileOrUrl) {\n if (!this.isNodeJs()) return localStorage.getItem(fileOrUrl) || \"\"\n const isUrl = fileOrUrl.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return this.root.readFile(fileOrUrl)\n return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))\n }\n async fetch(url, filename) {\n const isUrl = url.match(/^https?\\:[^ ]+$/)\n if (!isUrl) return\n return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)\n }\n get path() {\n return require(\"path\")\n }\n makeFullPath(filename) {\n return this.path.join(this.folderPath, filename)\n }\n _nextAndPrevious(arr, index) {\n const nextIndex = index + 1\n const previousIndex = index - 1\n return {\n previous: arr[previousIndex] ?? arr[arr.length - 1],\n next: arr[nextIndex] ?? arr[0]\n }\n }\n // keyboard nav is always in the same folder. does not currently support cross folder\n includeFileInKeyboardNav(file) {\n const { scrollProgram } = file\n return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)\n }\n get timeIndex() {\n return this.file.timeIndex || 0\n }\n get linkToPrevious() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous\n }\n return file.scrollProgram.permalink\n }\n importRegex = /^(import |[a-zA-Z\\_\\-\\.0-9\\/]+\\.(scroll|parsers)$|https?:\\/\\/.+\\.(scroll|parsers)$)/gm\n get linkToNext() {\n if (!this.hasKeyboardNav)\n // Dont provide link to next unless keyboard nav is on\n return undefined\n const {allScrollFiles} = this\n let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next\n while (!this.includeFileInKeyboardNav(file)) {\n file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next\n }\n return file.scrollProgram.permalink\n }\n // todo: clean up this naming pattern and add a parser instead of special casing 404.html\n get allHtmlFiles() {\n return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== \"404.html\")\n }\n parseNestedTag(tag) {\n if (!tag.includes(\"/\")) return;\n const {path} = this\n const parts = tag.split(\"/\")\n const group = parts.pop()\n const relativePath = parts.join(\"/\")\n return {\n group,\n relativePath,\n folderPath: path.join(this.folderPath, path.normalize(relativePath))\n }\n }\n getFilesByTags(tags, limit) {\n // todo: tags is currently matching partial substrings\n const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))\n if (typeof tags === \"string\") tags = tags.split(\" \")\n if (!tags || !tags.length)\n return this.allHtmlFiles\n .filter(file => file !== this) // avoid infinite loops. todo: think this through better.\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n let arr = []\n tags.forEach(tag => {\n if (!tag.includes(\"/\"))\n return (arr = arr.concat(\n getFilesWithTag(tag, this.allScrollFiles)\n .map(file => {\n return { file, relativePath: \"\" }\n })\n .slice(0, limit)\n ))\n const {folderPath, group, relativePath} = this.parseNestedTag(tag)\n let files = []\n try {\n files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)\n } catch (err) {\n console.error(err)\n }\n const filtered = getFilesWithTag(group, files).map(file => {\n return { file, relativePath: relativePath + \"/\" }\n })\n arr = arr.concat(filtered.slice(0, limit))\n })\n return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()\n }\n async fetchNode(url, filename) {\n filename = filename || new URL(url).pathname.split('/').pop()\n const fullpath = this.makeFullPath(filename)\n if (require(\"fs\").existsSync(fullpath)) return this.readFile(fullpath)\n this.log(`🛜 fetching ${url} to ${fullpath} `)\n await this.downloadToDisk(url, fullpath)\n return this.readFile(fullpath)\n }\n log(message) {\n if (this.logger) this.logger.log(message)\n }\n async fetchBrowser(url) {\n const content = localStorage.getItem(url)\n if (content) return content\n return this.downloadToLocalStorage(url)\n }\n async downloadToDisk(url, destination) {\n const { writeFile } = require('fs').promises\n const response = await fetch(url)\n const fileBuffer = await response.arrayBuffer()\n await writeFile(destination, Buffer.from(fileBuffer))\n return this.readFile(destination)\n }\n async downloadToLocalStorage(url) {\n const response = await fetch(url)\n const blob = await response.blob()\n localStorage.setItem(url, await blob.text())\n return localStorage.getItem(url)\n }\n readFile(filename) {\n const {path} = this\n const fs = require(\"fs\")\n const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, \"\"))\n if (fs.existsSync(fullPath))\n return fs.readFileSync(fullPath, \"utf8\")\n console.error(`File '${filename}' not found`)\n return \"\"\n }\n alreadyRequired = new Set()\n buildHtmlSnippet(buildSettings) {\n this.sectionStack = []\n return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))\n .filter(i => i)\n .join(\"\\n\")\n .trim() + this.clearSectionStack()\n }\n get footnotes() {\n if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)\n return this._footnotes\n }\n get authors() {\n return this.get(\"authors\")\n }\n get allScrollFiles() {\n try {\n return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)\n } catch (err) {\n console.error(err)\n return []\n }\n }\n async doThing(thing) {\n await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))\n }\n async load() {\n await this.doThing(\"load\")\n }\n async execute() {\n await this.doThing(\"execute\")\n }\n file = {}\n getFromParserId(parserId) {\n return this.parserIdIndex[parserId]?.[0].content\n }\n get fileSystem() {\n return this.file.fileSystem\n }\n get filePath() {\n return this.file.filePath\n }\n get folderPath() {\n return this.file.folderPath\n }\n get filename() {\n return this.file.filename || \"\"\n }\n get hasKeyboardNav() {\n return this.has(\"keyboardNav\")\n }\n get editHtml() {\n return `<a href=\"${this.editUrl}\" class=\"abstractTextLinkParser\">Edit</a>`\n }\n get externalsPath() {\n return this.file.EXTERNALS_PATH\n }\n get endSnippetIndex() {\n // Get the line number that the snippet should stop at.\n // First if its hard coded, use that\n if (this.has(\"endSnippet\")) return this.getParticle(\"endSnippet\").index\n // Next look for a dinkus\n const snippetBreak = this.find(particle => particle.isDinkus)\n if (snippetBreak) return snippetBreak.index\n return -1\n }\n get parserIds() {\n return this.topDownArray.map(particle => particle.definition.id)\n }\n get tags() {\n return this.get(\"tags\") || \"\"\n }\n get primaryTag() {\n return this.tags.split(\" \")[0]\n }\n get filenameNoExtension() {\n return this.filename.replace(\".scroll\", \"\")\n }\n // todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)\n // BaseUrl must be provided for RSS Feeds and OpenGraph tags to work\n get baseUrl() {\n const baseUrl = (this.get(\"baseUrl\") || \"\").replace(/\\/$/, \"\")\n return baseUrl + \"/\"\n }\n get canonicalUrl() {\n return this.get(\"canonicalUrl\") || this.baseUrl + this.permalink\n }\n get openGraphImage() {\n const openGraphImage = this.get(\"openGraphImage\")\n if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)\n const images = this.filter(particle => particle.doesExtend(\"scrollImageParser\"))\n const hit = images.find(particle => particle.has(\"openGraph\")) || images[0]\n if (!hit) return \"\"\n return this.ensureAbsoluteLink(hit.filename)\n }\n get absoluteLink() {\n return this.ensureAbsoluteLink(this.permalink)\n }\n ensureAbsoluteLink(link) {\n if (link.includes(\"://\")) return link\n return this.baseUrl + link.replace(/^\\//, \"\")\n }\n get editUrl() {\n const editUrl = this.get(\"editUrl\")\n if (editUrl) return editUrl\n const editBaseUrl = this.get(\"editBaseUrl\")\n return (editBaseUrl ? editBaseUrl.replace(/\\/$/, \"\") + \"/\" : \"\") + this.filename\n }\n get gitRepo() {\n // given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll\n // return https://github.com/breck7/breckyunits.com\n return this.editUrl.split(\"/\").slice(0, 5).join(\"/\")\n }\n get scrollVersion() {\n // currently manually updated\n return \"161.0.4\"\n }\n // Use the first paragraph for the description\n // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)\n // would speed up a lot.\n get description() {\n const description = this.getFromParserId(\"openGraphDescriptionParser\")\n if (description) return description\n return this.generatedDescription\n }\n get generatedDescription() {\n const firstParagraph = this.find(particle => particle.isArticleContent)\n return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&\"<>']/g, \"\") : \"\"\n }\n get titleFromFilename() {\n const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, \"$1 $2\").replace(/^./, match => match.toUpperCase())\n return unCamelCase(this.filenameNoExtension)\n }\n get title() {\n return this.getFromParserId(\"scrollTitleParser\") || this.titleFromFilename\n }\n get linkTitle() {\n return this.getFromParserId(\"scrollLinkTitleParser\") || this.title\n }\n get permalink() {\n return this.get(\"permalink\") || (this.filename ? this.filenameNoExtension + \".html\" : \"\")\n }\n compileTo(extensionCapitalized) {\n if (extensionCapitalized === \"Txt\")\n return this.asTxt\n if (extensionCapitalized === \"Html\")\n return this.asHtml\n const methodName = \"build\" + extensionCapitalized\n return this.topDownArray\n .filter(particle => particle[methodName])\n .map((particle, index) => particle[methodName](index))\n .join(\"\\n\")\n .trim()\n }\n get asTxt() {\n return (\n this.map(particle => {\n const text = particle.buildTxt ? particle.buildTxt() : \"\"\n if (text) return text + \"\\n\"\n if (!particle.getLine().length) return \"\\n\"\n return \"\"\n })\n .join(\"\")\n .replace(/<[^>]*>/g, \"\")\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .trim() + \"\\n\" // Always end in a newline, Posix style\n )\n }\n get dependencies() {\n const dependencies = this.file.dependencies?.slice() || []\n const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()\n return dependencies.concat(files)\n }\n get buildsHtml() {\n const { permalink } = this\n return !this.file.importOnly && (permalink.endsWith(\".html\") || permalink.endsWith(\".htm\"))\n }\n // Without specifying the language hyphenation will not work.\n get lang() {\n return this.get(\"htmlLang\") || \"en\"\n }\n _compiledHtml = \"\"\n get asHtml() {\n if (!this._compiledHtml) {\n const { permalink, buildsHtml } = this\n const content = (this.buildHtml() + this.clearBodyStack()).trim()\n // Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But\n // <1% of use case so might be good enough.\n const wrapWithHtmlTags = buildsHtml\n const bodyTag = this.has(\"metaTags\") ? \"\" : \"<body>\\n\"\n this._compiledHtml = wrapWithHtmlTags ? `<!DOCTYPE html>\\n<html lang=\"${this.lang}\">\\n${bodyTag}${content}\\n</body>\\n</html>` : content\n }\n return this._compiledHtml\n }\n get wordCount() {\n return this.asTxt.match(/\\b\\w+\\b/g)?.length || 0\n }\n get minutes() {\n return parseFloat((this.wordCount / 200).toFixed(1))\n }\n get date() {\n const date = this.get(\"date\") || (this.file.timestamp ? this.file.timestamp : 0)\n return this.dayjs(date).format(`MM/DD/YYYY`)\n }\n get year() {\n return parseInt(this.dayjs(this.date).format(`YYYY`))\n }\n get dayjs() {\n if (!this.isNodeJs()) return dayjs\n const lib = require(\"dayjs\")\n const relativeTime = require(\"dayjs/plugin/relativeTime\")\n lib.extend(relativeTime)\n return lib\n }\n get lodash() {\n return this.isNodeJs() ? require(\"lodash\") : lodash\n }\n getConcepts(parsed) {\n const concepts = []\n let currentConcept\n parsed.forEach(particle => {\n if (particle.isConceptDelimiter) {\n if (currentConcept) concepts.push(currentConcept)\n currentConcept = []\n }\n if (currentConcept && particle.isMeasure) currentConcept.push(particle)\n })\n if (currentConcept) concepts.push(currentConcept)\n return concepts\n }\n _formatConcepts(parsed) {\n const concepts = this.getConcepts(parsed)\n if (!concepts.length) return false\n const {lodash} = this\n // does a destructive sort in place on the parsed program\n concepts.forEach(concept => {\n let currentSection\n const newCode = lodash\n .sortBy(concept, [\"sortIndex\"])\n .map(particle => {\n let newLines = \"\"\n const section = particle.sortIndex.toString().split(\".\")[0]\n if (section !== currentSection) {\n currentSection = section\n newLines = \"\\n\"\n }\n return newLines + particle.toString()\n })\n .join(\"\\n\")\n concept.forEach((particle, index) => (index ? particle.destroy() : \"\"))\n concept[0].replaceParticle(() => newCode)\n })\n }\n get formatted() {\n return this.getFormatted(this.file.codeAtStart)\n }\n get lastCommitTime() {\n // todo: speed this up and do a proper release. also could add more metrics like this.\n if (this._lastCommitTime === undefined) {\n try {\n this._lastCommitTime = require(\"child_process\").execSync(`git log -1 --format=\"%at\" -- \"${this.filePath}\"`).toString().trim()\n } catch (err) {\n this._lastCommitTime = 0\n }\n }\n return this._lastCommitTime\n }\n getFormatted(codeAtStart = this.toString()) {\n let formatted = codeAtStart.replace(/\\r/g, \"\") // remove all carriage returns if there are any\n const parsed = new this.constructor(formatted)\n parsed.topDownArray.forEach(subparticle => {\n subparticle.format()\n const original = subparticle.getLine()\n const trimmed = original.replace(/(\\S.*?)[ \\t]*$/gm, \"$1\")\n // Trim trailing whitespace unless parser allows it\n if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)\n })\n this._formatConcepts(parsed)\n let importOnlys = []\n let topMatter = []\n let allElse = []\n // Create any bindings\n parsed.forEach(particle => {\n if (particle.bindTo === \"next\") particle.binding = particle.next\n if (particle.bindTo === \"previous\") particle.binding = particle.previous\n })\n parsed.forEach(particle => {\n if (particle.getLine() === \"importOnly\") importOnlys.push(particle)\n else if (particle.isTopMatter) topMatter.push(particle)\n else allElse.push(particle)\n })\n const combined = importOnlys.concat(topMatter, allElse)\n // Move any bound particles\n combined\n .filter(particle => particle.bindTo)\n .forEach(particle => {\n // First remove the particle from its current position\n const originalIndex = combined.indexOf(particle)\n combined.splice(originalIndex, 1)\n // Then insert it at the new position\n // We need to find the binding index again after removal\n const bindingIndex = combined.indexOf(particle.binding)\n if (particle.bindTo === \"next\") combined.splice(bindingIndex, 0, particle)\n else combined.splice(bindingIndex + 1, 0, particle)\n })\n const trimmed = combined\n .map(particle => particle.toString())\n .join(\"\\n\")\n .replace(/^\\n*/, \"\") // Remove leading newlines\n .replace(/\\n\\n\\n+/g, \"\\n\\n\") // Maximum 2 newlines in a row\n .replace(/\\n+$/, \"\")\n return trimmed === \"\" ? trimmed : trimmed + \"\\n\" // End non blank Scroll files in a newline character POSIX style for better working with tools like git\n }\n get parser() {\n return this.constructor\n }\n get parsersRequiringExternals() {\n const { parser } = this\n // todo: could be cleaned up a bit\n if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])\n return parser.parsersRequiringExternals\n }\n get Disk() { return this.isNodeJs() ? require(\"scrollsdk/products/Disk.node.js\").Disk : {}}\n async buildAll() {\n await this.load()\n await this.buildOne()\n await this.buildTwo()\n }\n async buildOne() {\n await this.execute()\n const toBuild = this.filter(particle => particle.buildOne)\n for (let particle of toBuild) {\n await particle.buildOne()\n }\n }\n async buildTwo(externalFilesCopied = {}) {\n const toBuild = this.filter(particle => particle.buildTwo)\n for (let particle of toBuild) {\n await particle.buildTwo(externalFilesCopied)\n }\n }\n get outputFileNames() {\n return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()\n }\n _compileArray(filename, arr) {\n const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== \"\")))\n const parts = filename.split(\".\")\n const format = parts.pop()\n if (format === \"json\") return JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"js\") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)\n if (format === \"csv\") return this.arrayToCSV(arr)\n if (format === \"tsv\") return this.arrayToCSV(arr, \"\\t\")\n if (format === \"particles\") return particles.toString()\n return particles.toString()\n }\n makeLodashOrderByParams(str) {\n const part1 = str.split(\" \")\n const part2 = part1.map(col => (col.startsWith(\"-\") ? \"desc\" : \"asc\"))\n return [part1.map(col => col.replace(/^\\-/, \"\")), part2]\n }\n arrayToCSV(data, delimiter = \",\") {\n if (!data.length) return \"\"\n // Extract headers\n const headers = Object.keys(data[0])\n const csv = data.map(row =>\n headers\n .map(fieldName => {\n const fieldValue = row[fieldName]\n // Escape commas if the value is a string\n if (typeof fieldValue === \"string\" && fieldValue.includes(delimiter)) {\n return `\"${fieldValue.replace(/\"/g, '\"\"')}\"` // Escape double quotes and wrap in double quotes\n }\n return fieldValue\n })\n .join(delimiter)\n )\n csv.unshift(headers.join(delimiter)) // Add header row at the top\n return csv.join(\"\\n\")\n }\n compileConcepts(filename = \"csv\", sortBy = \"\") {\n const {lodash} = this\n if (!sortBy) return this._compileArray(filename, this.concepts)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))\n }\n _withStats\n get measuresWithStats() {\n if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)\n return this._withStats\n }\n addMeasureStats(concepts, measures){\n return measures.map(measure => {\n let Type = false\n concepts.forEach(concept => {\n const value = concept[measure.Name]\n if (value === undefined || value === \"\") return\n measure.Values++\n if (!Type) {\n measure.Example = value.toString().replace(/\\n/g, \" \")\n measure.Type = typeof value\n Type = true\n }\n })\n measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + \"%\"\n return measure\n })\n }\n parseMeasures(parser) {\n if (!Particle.measureCache)\n Particle.measureCache = new Map()\n const measureCache = Particle.measureCache\n if (measureCache.get(parser)) return measureCache.get(parser)\n const {lodash} = this\n // todo: clean this up\n const getCueAtoms = rootParserProgram =>\n rootParserProgram\n .filter(particle => particle.getLine().endsWith(\"Parser\") && !particle.getLine().startsWith(\"abstract\"))\n .map(particle => particle.get(\"cue\") || particle.getLine())\n .map(line => line.replace(/Parser$/, \"\"))\n // Generate a fake program with one of every of the available parsers. Then parse it. Then we can easily access the meta data on the parsers\n const dummyProgram = new parser(\n Array.from(\n new Set(\n getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?\n )\n ).join(\"\\n\")\n )\n // Delete any particles that are not measures\n dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n dummyProgram.forEach(particle => {\n // add nested measures\n Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))\n })\n // Delete any nested particles that are not measures\n dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())\n const measures = dummyProgram.topDownArray.map(particle => {\n return {\n Name: particle.measureName,\n Values: 0,\n Coverage: 0,\n Question: particle.definition.description,\n Example: particle.definition.getParticle(\"example\")?.subparticlesToString() || \"\",\n Type: particle.typeForWebForms,\n Source: particle.sourceDomain,\n //Definition: parsedProgram.root.filename + \":\" + particle.lineNumber\n SortIndex: particle.sortIndex,\n IsComputed: particle.isComputed,\n IsRequired: particle.isMeasureRequired,\n IsConceptDelimiter: particle.isConceptDelimiter,\n Cue: particle.definition.get(\"cue\")\n }\n })\n measureCache.set(parser, lodash.sortBy(measures, \"SortIndex\"))\n return measureCache.get(parser)\n }\n _concepts\n get concepts() {\n if (this._concepts) return this._concepts\n this._concepts = this.parseConcepts(this, this.measures)\n return this._concepts\n }\n _measures\n get measures() {\n if (this._measures) return this._measures\n this._measures = this.parseMeasures(this.parser)\n return this._measures\n }\n parseConcepts(parsedProgram, measures){\n // Todo: might be a perf/memory/simplicity win to have a \"segment\" method in ScrollSDK, where you could\n // virtually split a Particle into multiple segments, and then query on those segments.\n // So we would \"segment\" on \"id \", and then not need to create a bunch of new objects, and the original\n // already parsed lines could then learn about/access to their respective segments.\n const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]\n if (!conceptDelimiter) return []\n const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)\n concepts.shift() // Remove the part before \"id\"\n return concepts.map(concept => {\n const row = {}\n measures.forEach(measure => {\n const measureName = measure.Name\n const measureKey = measure.Cue || measureName.replace(/_/g, \" \")\n if (!measure.IsComputed) row[measureName] = concept.getParticle(measureKey)?.measureValue ?? \"\"\n else row[measureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)\n })\n return row\n })\n }\n computeMeasure(parsedProgram, measureName, concept, concepts){\n // note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll\n if (!Particle.measureFnCache) Particle.measureFnCache = {}\n const measureFnCache = Particle.measureFnCache\n if (!measureFnCache[measureName]) {\n // a bit hacky but works??\n const particle = parsedProgram.appendLine(measureName)\n measureFnCache[measureName] = particle.computeValue\n particle.destroy()\n }\n return measureFnCache[measureName](concept, measureName, parsedProgram, concepts)\n }\n compileMeasures(filename = \"csv\", sortBy = \"\") {\n const withStats = this.measuresWithStats\n if (!sortBy) return this._compileArray(filename, withStats)\n const orderBy = this.makeLodashOrderByParams(sortBy)\n return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))\n }\n evalNodeJsMacros(value, macroMap, filePath) {\n const tempPath = filePath + \".js\"\n const {Disk} = this\n if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)\n try {\n Disk.write(tempPath, value)\n const results = require(tempPath)\n Object.keys(results).forEach(key => (macroMap[key] = results[key]))\n } catch (err) {\n console.error(`Error in evalMacros in file '${filePath}'`)\n console.error(err)\n } finally {\n Disk.rm(tempPath)\n }\n }\n evalMacros(fusedFile) {\n const {fusedCode, codeAtStart, filePath} = fusedFile\n let code = fusedCode\n const absolutePath = filePath\n // note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)\n const regex = /^(replace|footer$)/gm\n if (!regex.test(code)) return code\n const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?\n // Process macros\n const macroMap = {}\n particle\n .filter(particle => {\n const parserAtom = particle.cue\n return parserAtom === \"replace\" || parserAtom === \"replaceJs\" || parserAtom === \"replaceNodejs\"\n })\n .forEach(particle => {\n let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(\" \")\n const kind = particle.cue\n if (kind === \"replaceJs\") value = eval(value)\n if (this.isNodeJs() && kind === \"replaceNodejs\")\n this.evalNodeJsMacros(value, macroMap, absolutePath)\n else macroMap[particle.getAtom(1)] = value\n particle.destroy() // Destroy definitions after eval\n })\n if (particle.has(\"footer\")) {\n const pushes = particle.getParticles(\"footer\")\n const append = pushes.map(push => push.section.join(\"\\n\")).join(\"\\n\")\n pushes.forEach(push => {\n push.section.forEach(particle => particle.destroy())\n push.destroy()\n })\n code = particle.asString + append\n }\n const keys = Object.keys(macroMap)\n if (!keys.length) return code\n let codeAfterMacroSubstitution = particle.asString\n // Todo: speed up. build a template?\n Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, \"g\"), macroMap[key])))\n return codeAfterMacroSubstitution\n }\n toRss() {\n const { title, canonicalUrl } = this\n return ` <item>\n <title>${title}</title>\n <link>${canonicalUrl}</link>\n <pubDate>${this.dayjs(this.timestamp * 1000).format(\"ddd, DD MMM YYYY HH:mm:ss ZZ\")}</pubDate>\n </item>`\n }\n example\n # Hello world\n ## This is Scroll\n * It compiles to HTML.\n \n code\n // You can add code as well.\n print(\"Hello world\")\nstampFileParser\n catchAllAtomType stringAtom\n description Create a file.\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const fullPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating file ${fullPath}`)\n fs.mkdirSync(path.dirname(fullPath), {recursive: true})\n const content = this.subparticlesToString()\n fs.writeFileSync(fullPath, content, \"utf8\")\n const isExecutable = content.startsWith(\"#!\")\n if (isExecutable) fs.chmodSync(fullPath, \"755\")\n }\nstampFolderParser\n catchAllAtomType stringAtom\n description Create a folder.\n inScope stampFolderParser\n catchAllParser stampFileParser\n pattern \\/$\n javascript\n execute(parentDir) {\n const fs = require(\"fs\")\n const path = require(\"path\")\n const newPath = path.join(parentDir, this.getLine())\n this.root.log(`Creating folder ${newPath}`)\n fs.mkdirSync(newPath, {recursive: true})\n this.forEach(particle => particle.execute(newPath))\n }\nstumpContentParser\n popularity 0.102322\n catchAllAtomType anyAtom\nscrollTableDataParser\n popularity 0.001061\n cue data\n description Table from inline delimited data.\n catchAllAtomType anyAtom\n baseParser blobParser\nscrollTableDelimiterParser\n popularity 0.001037\n description Set the delimiter.\n cue delimiter\n atoms cueAtom stringAtom\n javascript\n buildHtml() {\n return \"\"\n }\nplainTextLineParser\n popularity 0.000121\n catchAllAtomType stringAtom\n catchAllParser plainTextLineParser"}
\ No newline at end of file
diff --git a/package.json b/package.json
index e6898ac..bece262 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
},
"dependencies": {
"jquery": "^3.6.0",
- "scroll-cli": "^161.0.3",
+ "scroll-cli": "^161.1.0",
"scrollsdk": "^99.0.0"
},
"devDependencies": {
diff --git a/scroll.parsers b/scroll.parsers
index 7faac14..745d63e 100644
--- a/scroll.parsers
+++ b/scroll.parsers
@@ -1440,17 +1440,19 @@ abstractBuildCommandParser
buildOutput() {
return this.root.compileTo(this.extension)
}
+ get outputFileNames() {
+ return this.content?.split(" ") || [this.root.permalink.replace(".html", "." + this.extension.toLowerCase())]
+ }
async _buildFileType(extension) {
const {root} = this
- const { fileSystem, folderPath, filename, filePath, path, lodash, permalink } = root
+ const { fileSystem, folderPath, filename, filePath, path, lodash } = root
const capitalized = lodash.capitalize(extension)
const buildKeyword = "build" + capitalized
- const outputFiles = this.content?.split(" ") || [""]
- for (let name of outputFiles) {
- const link = name || permalink.replace(".html", "." + extension.toLowerCase())
+ const {outputFileNames} = this
+ for (let name of outputFileNames) {
try {
- await fileSystem.writeProduct(path.join(folderPath, link), root.compileTo(capitalized))
- root.log(`💾 Built ${link} from ${filename}`)
+ await fileSystem.writeProduct(path.join(folderPath, name), root.compileTo(capitalized))
+ root.log(`💾 Built ${name} from ${filename}`)
} catch (err) {
console.error(`Error while building '${filePath}' with extension '${extension}'`)
throw err
@@ -5645,7 +5647,7 @@ scrollParser
}
get scrollVersion() {
// currently manually updated
- return "161.0.3"
+ return "161.0.4"
}
// Use the first paragraph for the description
// todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?)
@@ -5871,6 +5873,9 @@ scrollParser
await particle.buildTwo(externalFilesCopied)
}
}
+ get outputFileNames() {
+ return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()
+ }
_compileArray(filename, arr) {
const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== "")))
const parts = filename.split(".")