Atom Definition Parsers analyze the atoms in a line.
" + this.docToHtml(this.sortDocs(this.atomParsersToDocument))
}
makeLink() {
return ""
}
categories = "assemblePhase acquirePhase analyzePhase actPhase".split(" ")
getCategory(tags) {
return tags.split(" ").filter(w => w.endsWith("Phase"))[0]
}
getNote(category) {
return ` A${category.replace("Phase", "").substr(1)}Time.`
}
get atomParsersToDocument() {
const parsersParser = require("scrollsdk/products/parsers.nodejs.js")
const clone = new parsersParser("anyAtom\n ").clone()
const parserParticle = clone.getParticle("anyAtom")
const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)
atoms.sort()
parserParticle.setSubparticles(atoms.join("\n"))
return parserParticle
}
get parsersToDocument() {
const parsersParser = require("scrollsdk/products/parsers.nodejs.js")
const clone = new parsersParser("latinParser\n ").clone()
const parserParticle = clone.getParticle("latinParser")
const atoms = clone.getAutocompleteResultsAt(1,1).matches.map(a => a.text)
atoms.sort()
parserParticle.setSubparticles(atoms.join("\n"))
clone.appendLine("myParser")
clone.appendLine("myAtom")
return parserParticle
}
}
class abstractMeasureParser extends abstractScrollParser {
get measureNameAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
get typeForWebForms() { return `text` }
get isComputed() { return false }
get sortIndex() { return 1.9 }
get isMeasure() { return true }
buildHtmlSnippet() {
return ""
}
buildHtml() {
return ""
}
get measureValue() {
return this.content ?? ""
}
get measureName() {
return this.getCuePath().replace(/ /g, "_")
}
}
class abstractAtomMeasureParser extends abstractMeasureParser {
get measureNameAtom() {
return this.getAtom(0)
}
get atomAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
}
class abstractEmailMeasureParser extends abstractAtomMeasureParser {
get measureNameAtom() {
return this.getAtom(0)
}
get emailAddressAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
get typeForWebForms() { return `email` }
}
class abstractUrlMeasureParser extends abstractAtomMeasureParser {
get measureNameAtom() {
return this.getAtom(0)
}
get urlAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
get typeForWebForms() { return `url` }
}
class abstractStringMeasureParser extends abstractMeasureParser {
get stringAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class abstractIdParser extends abstractStringMeasureParser {
get suggestInAutocomplete() { return false }
get isConceptDelimiter() { return true }
get isMeasureRequired() { return true }
get sortIndex() { return 1 }
getErrors() {
const errors = super.getErrors()
let requiredMeasureNames = this.root.measures.filter(measure => measure.isMeasureRequired).map(measure => measure.Name).filter(name => name !== "id")
if (!requiredMeasureNames.length) return errors
let next = this.next
while (requiredMeasureNames.length && next.cue !== "id" && next.index !== 0) {
requiredMeasureNames = requiredMeasureNames.filter(i => i !== next.cue)
next = next.next
}
requiredMeasureNames.forEach(name =>
errors.push(this.makeError(`Concept "${this.content}" is missing required measure "${name}".`))
)
return errors
}
}
class abstractTextareaMeasureParser extends abstractMeasureParser {
createParserCombinator() { return new Particle.ParserCombinator(this._getBlobParserCatchAllParser())}
getErrors() { return [] }
get suggestInAutocomplete() { return false }
get typeForWebForms() { return `textarea` }
get measureValue() {
return this.subparticlesToString().replace(/\n/g, "\\n")
}
}
class abstractNumericMeasureParser extends abstractMeasureParser {
get suggestInAutocomplete() { return false }
get typeForWebForms() { return `number` }
get measureValue() {
const {content} = this
return content === undefined ? "" : parseFloat(content)
}
}
class abstractIntegerMeasureParser extends abstractNumericMeasureParser {
get measureNameAtom() {
return this.getAtom(0)
}
get integerAtom() {
return parseInt(this.getAtom(1))
}
get suggestInAutocomplete() { return false }
}
class abstractFloatMeasureParser extends abstractNumericMeasureParser {
get measureNameAtom() {
return this.getAtom(0)
}
get floatAtom() {
return parseFloat(this.getAtom(1))
}
get suggestInAutocomplete() { return false }
}
class abstractPercentageMeasureParser extends abstractNumericMeasureParser {
get measureNameAtom() {
return this.getAtom(0)
}
get percentAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
get measureValue() {
const {content} = this
return content === undefined ? "" : parseFloat(content)
}
}
class abstractEnumMeasureParser extends abstractMeasureParser {
get measureNameAtom() {
return this.getAtom(0)
}
get enumAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
}
class abstractBooleanMeasureParser extends abstractMeasureParser {
get measureNameAtom() {
return this.getAtom(0)
}
get booleanAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
get measureValue() {
const {content} = this
return content === undefined ? "" : content == "true"
}
}
class metaTagsParser extends abstractScrollParser {
get suggestInAutocomplete() { return false }
buildHtmlSnippet() {
return ""
}
buildHtml() {
const {root} = this
const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage } = root
const rssFeedUrl = root.get("rssFeedUrl")
const favicon = root.get("favicon")
const faviconTag = favicon ? `` : ""
const rssTag = rssFeedUrl ? `` : ""
const gitTag = gitRepo ? `` : ""
return `
${title}
${faviconTag}
${gitTag}
${rssTag}
`
}
}
class quoteParser extends abstractScrollParser {
createParserCombinator() {
return new Particle.ParserCombinator(quoteLineParser, undefined, undefined)
}
get suggestInAutocomplete() { return false }
buildHtml() {
return `
${this.subparticlesToString()}
`
}
buildTxt() {
return this.subparticlesToString()
}
}
class redirectToParser extends abstractScrollParser {
get cueAtom() {
return this.getAtom(0)
}
get urlAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
buildHtml() {
return ``
}
}
class abstractVariableParser extends abstractScrollParser {
get preBuildCommandAtom() {
return this.getAtom(0)
}
get stringAtom() {
return this.getAtomsFrom(1)
}
get suggestInAutocomplete() { return false }
isTopMatter = true
buildHtml() {
return ""
}
}
class replaceParser extends abstractVariableParser {
createParserCombinator() { return new Particle.ParserCombinator(this._getBlobParserCatchAllParser())}
getErrors() { return [] }
get suggestInAutocomplete() { return false }
}
class replaceJsParser extends replaceParser {
createParserCombinator() { return new Particle.ParserCombinator(this._getBlobParserCatchAllParser())}
getErrors() { return [] }
get javascriptAnyAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class replaceNodejsParser extends abstractVariableParser {
createParserCombinator() { return new Particle.ParserCombinator(this._getBlobParserCatchAllParser())}
getErrors() { return [] }
get javascriptAnyAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class runScriptParser extends abstractScrollParser {
get cueAtom() {
return this.getAtom(0)
}
get urlAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
get filenameIndex() { return 1 }
get dependencies() { return [this.filename]}
results = "Not yet run"
async execute() {
if (!this.filename) return
await this.root.fetch(this.filename)
// todo: make async
const { execSync } = require("child_process")
this.results = execSync(this.command)
}
get command() {
const path = this.root.path
const {filename }= this
const fullPath = this.root.makeFullPath(filename)
const ext = path.extname(filename).slice(1)
const interpreterMap = {
php: "php",
py: "python3",
rb: "ruby",
pl: "perl",
sh: "sh"
}
return [interpreterMap[ext], fullPath].join(" ")
}
buildHtml() {
return this.buildTxt()
}
get filename() {
return this.getAtom(this.filenameIndex)
}
buildTxt() {
return this.results.toString().trim()
}
}
class quickRunScriptParser extends runScriptParser {
get urlAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
get filenameIndex() { return 0 }
}
class endSnippetParser extends abstractScrollParser {
get suggestInAutocomplete() { return false }
buildHtml() {
return ""
}
}
class toStampParser extends abstractScrollParser {
get filePathAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
buildTxt() {
return this.makeStamp(this.root.makeFullPath(this.content))
}
buildHtml() {
return `
${this.buildTxt()}
`
}
makeStamp(dir) {
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
let stamp = 'stamp\n';
const handleFile = (indentation, relativePath, itemPath, ) => {
stamp += `${indentation}${relativePath}\n`;
const content = fs.readFileSync(itemPath, 'utf8');
stamp += `${indentation} ${content.replace(/\n/g, `\n${indentation} `)}\n`;
}
let gitTrackedFiles
function processDirectory(currentPath, depth) {
const items = fs.readdirSync(currentPath);
items.forEach(item => {
const itemPath = path.join(currentPath, item);
const relativePath = path.relative(dir, itemPath);
if (!gitTrackedFiles.has(item)) return
const stats = fs.statSync(itemPath);
const indentation = ' '.repeat(depth);
if (stats.isDirectory()) {
stamp += `${indentation}${relativePath}/\n`;
processDirectory(itemPath, depth + 1);
} else if (stats.isFile())
handleFile(indentation, relativePath, itemPath)
});
}
const stats = fs.statSync(dir);
if (stats.isDirectory()) {
// Get list of git-tracked files
gitTrackedFiles = new Set(execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })
.split('\n')
.filter(Boolean))
processDirectory(dir, 1)
}
else
handleFile(" ", dir, dir)
return stamp.trim();
}
}
class stampParser extends abstractScrollParser {
createParserCombinator() {
return new Particle.ParserCombinator(stampFileParser, undefined, [{regex: /\/$/, parser: stampFolderParser}])
}
get preBuildCommandAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
execute() {
const dir = this.root.folderPath
this.forEach(particle => particle.execute(dir))
}
}
class scrollStumpParser extends abstractScrollParser {
createParserCombinator() {
return new Particle.ParserCombinator(stumpContentParser, undefined, undefined)
}
get suggestInAutocomplete() { return false }
buildHtml() {
const {stumpParser} = this
return new stumpParser(this.subparticlesToString()).compile()
}
get stumpParser() {
return this.isNodeJs() ? require("scrollsdk/products/stump.nodejs.js") : stumpParser
}
}
class stumpNoSnippetParser extends scrollStumpParser {
get suggestInAutocomplete() { return false }
buildHtmlSnippet() {
return ""
}
}
class plainTextParser extends abstractScrollParser {
createParserCombinator() {
return new Particle.ParserCombinator(plainTextLineParser, undefined, undefined)
}
get stringAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
buildHtml() {
return this.buildTxt()
}
buildTxt() {
return `${this.content ?? ""}${this.subparticlesToString()}`
}
}
class plainTextOnlyParser extends plainTextParser {
get suggestInAutocomplete() { return false }
buildHtml() {
return ""
}
}
class scrollThemeParser extends abstractScrollParser {
get scrollThemeAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
get copyFromExternal() { return `// Note this will be replaced at runtime` }
get isPopular() { return true }
get copyFromExternal() {
return this.files.join(" ")
}
get files() {
return this.atoms.slice(1).map(name => `${name}.css`)
}
buildHtml() {
return this.files.map(name => ``).join("\n")
}
}
class abstractAftertextAttributeParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
get isAttribute() { return true }
get htmlAttributes() {
return `${this.cue}="${this.content}"`
}
buildHtml() {
return ""
}
}
class aftertextIdParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get htmlIdAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
}
class aftertextStyleParser extends abstractAftertextAttributeParser {
get cssAnyAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
htmlAttributes = "" // special case this one
get css() { return `${this.property}:${this.content};` }
}
class aftertextFontParser extends aftertextStyleParser {
get cueAtom() {
return this.getAtom(0)
}
get fontFamilyAtom() {
return this.getAtom(1)
}
get cssAnyAtom() {
return this.getAtomsFrom(2)
}
get suggestInAutocomplete() { return false }
get property() { return `font-family` }
get css() {
if (this.content === "Slim") return "font-family:Helvetica Neue; font-weight:100;"
return super.css
}
}
class aftertextColorParser extends aftertextStyleParser {
get cssAnyAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
get property() { return `color` }
}
class aftertextOnclickParser extends abstractAftertextAttributeParser {
get anyAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class aftertextHiddenParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
}
class aftertextTagParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get htmlTagAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
buildHtml() {
return ""
}
}
class abstractAftertextDirectiveParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get stringAtom() {
return this.getAtomsFrom(1)
}
get suggestInAutocomplete() { return false }
isMarkup = true
buildHtml() {
return ""
}
getErrors() {
const errors = super.getErrors()
if (!this.isMarkup || this.matchWholeLine) return errors
const inserts = this.getInserts(this.parent.originalTextPostLinkify)
// todo: make AbstractParticleError class exported by sdk to allow Parsers to define their own error types.
// todo: also need to be able to map lines back to their line in source (pre-imports)
if (!inserts.length)
errors.push(this.makeError(`No match found for "${this.getLine()}".`))
return errors
}
get pattern() {
return this.getAtomsFrom(1).join(" ")
}
get shouldMatchAll() {
return this.has("matchAll")
}
getMatches(text) {
const { pattern } = this
const escapedPattern = pattern.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")
return [...text.matchAll(new RegExp(escapedPattern, "g"))].map(match => {
const { index } = match
const endIndex = index + pattern.length
return [
{ index, string: `<${this.openTag}${this.allAttributes}>`, endIndex },
{ index: endIndex, endIndex, string: `${this.closeTag}>` }
]
})
}
getInserts(text) {
const matches = this.getMatches(text)
if (!matches.length) return false
if (this.shouldMatchAll) return matches.flat()
const match = this.getParticle("match")
if (match)
return match.indexes
.map(index => matches[index])
.filter(i => i)
.flat()
return matches[0]
}
get allAttributes() {
const attr = this.attributes.join(" ")
return attr ? " " + attr : ""
}
get attributes() {
return []
}
get openTag() {
return this.tag
}
get closeTag() {
return this.tag
}
}
class abstractMarkupParser extends abstractAftertextDirectiveParser {
createParserCombinator() {
return new Particle.ParserCombinator(undefined, Object.assign(Object.assign({}, super.createParserCombinator()._getCueMapAsObject()), {"matchAll" : matchAllParser,
"match" : matchParser}), undefined)
}
get suggestInAutocomplete() { return false }
get matchWholeLine() {
return this.getAtomsFrom(this.patternStartsAtAtom).length === 0
}
get pattern() {
return this.matchWholeLine ? this.parent.originalText : this.getAtomsFrom(this.patternStartsAtAtom).join(" ")
}
patternStartsAtAtom = 1
}
class boldParser extends abstractMarkupParser {
get suggestInAutocomplete() { return false }
tag = "b"
}
class italicsParser extends abstractMarkupParser {
get suggestInAutocomplete() { return false }
tag = "i"
}
class underlineParser extends abstractMarkupParser {
get suggestInAutocomplete() { return false }
tag = "u"
}
class afterTextCenterParser extends abstractMarkupParser {
get suggestInAutocomplete() { return false }
tag = "center"
}
class aftertextCodeParser extends abstractMarkupParser {
get suggestInAutocomplete() { return false }
tag = "code"
}
class aftertextStrikeParser extends abstractMarkupParser {
get suggestInAutocomplete() { return false }
tag = "s"
}
class classMarkupParser extends abstractMarkupParser {
get cueAtom() {
return this.getAtom(0)
}
get classNameAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
tag = "span"
get applyToParentElement() {
return this.atoms.length === 2
}
getInserts(text) {
// If no select text is added, set the class on the parent element.
if (this.applyToParentElement) return []
return super.getInserts(text)
}
get className() {
return this.getAtom(1)
}
get attributes() {
return [`class="${this.className}"`]
}
get matchWholeLine() {
return this.applyToParentElement
}
get pattern() {
return this.matchWholeLine ? this.parent.content : this.getAtomsFrom(2).join(" ")
}
}
class classesMarkupParser extends classMarkupParser {
get suggestInAutocomplete() { return false }
applyToParentElement = true
get className() {
return this.content
}
}
class hoverNoteParser extends classMarkupParser {
createParserCombinator() {
return new Particle.ParserCombinator(lineOfTextParser, undefined, undefined)
}
get cueAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
get pattern() {
return this.getAtomsFrom(1).join(" ")
}
get attributes() {
return [`class="scrollHoverNote"`, `title="${this.hoverNoteText}"`]
}
get hoverNoteText() {
return this.subparticlesToString().replace(/\n/g, " ")
}
}
class linkParser extends abstractMarkupParser {
createParserCombinator() {class programParser extends ParserBackedParticle {
createParserCombinator() {
return new Particle.ParserCombinator(programLinkParser, undefined, undefined)
}
get cueAtom() {
return this.getAtom(0)
}
get encoded() {
return encodeURIComponent(this.subparticlesToString())
}
}
return new Particle.ParserCombinator(undefined, Object.assign(Object.assign({}, super.createParserCombinator()._getCueMapAsObject()), {"comment" : commentParser,
"!" : counterpointParser,
"//" : slashCommentParser,
"thanksTo" : thanksToParser,
"target" : linkTargetParser,
"title" : linkTitleParser,
"program" : programParser}), undefined)
}
get cueAtom() {
return this.getAtom(0)
}
get urlAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
tag = "a"
buildTxt() {
return this.root.ensureAbsoluteLink(this.link) + " " + this.pattern
}
get link() {
const {baseLink} = this
if (this.has("program"))
return baseLink + this.getParticle("program").encoded
return baseLink
}
get baseLink() {
const link = this.getAtom(1)
const isAbsoluteLink = link.includes("://")
if (isAbsoluteLink) return link
const relativePath = this.parent.buildSettings?.relativePath || ""
return relativePath + link
}
get attributes() {
const attrs = [`href="${this.link}"`]
const options = ["title", "target"]
options.forEach(option => {
const particle = this.getParticle(option)
if (particle) attrs.push(`${option}="${particle.content}"`)
})
return attrs
}
patternStartsAtAtom = 2
}
class emailLinkParser extends linkParser {
get suggestInAutocomplete() { return false }
get attributes() {
return [`href="mailto:${this.link}"`]
}
}
class quickLinkParser extends linkParser {
get urlAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
get link() {
return this.cue
}
patternStartsAtAtom = 1
}
class quickRelativeLinkParser extends linkParser {
get urlAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
get link() {
return this.cue
}
patternStartsAtAtom = 1
}
class datelineParser extends abstractAftertextDirectiveParser {
get suggestInAutocomplete() { return false }
getInserts() {
const {day} = this
if (!day) return false
return [{ index: 0, string: `${day} — ` }]
}
matchWholeLine = true
get day() {
let day = this.content || this.root.date
if (!day) return ""
return this.root.dayjs(day).format(`MMMM D, YYYY`)
}
}
class dayjsParser extends abstractAftertextDirectiveParser {
get suggestInAutocomplete() { return false }
getInserts() {
const dayjs = this.root.dayjs
const days = eval(this.content)
const index = this.parent.originalTextPostLinkify.indexOf("days")
return [{ index, string: `${days} ` }]
}
}
class inlineMarkupsOnParser extends abstractAftertextDirectiveParser {
get inlineMarkupNameAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
get shouldMatchAll() {
return true
}
get markups() {
const {root} = this
let markups = [{delimiter: "`", tag: "code", exclusive: true, name: "code"},{delimiter: "*", tag: "strong", name: "bold"}, {delimiter: "_", tag: "em", name: "italics"}]
// only add katex markup if the root doc has katex.
if (root.has("katex"))
markups.unshift({delimiter: "$", tag: "span", attributes: ' class="scrollKatex"', exclusive: true, name: "katex"})
if (this.content)
return markups.filter(markup => this.content.includes(markup.name))
if (root.has("inlineMarkups")) {
root.getParticle("inlineMarkups").forEach(markup => {
const delimiter = markup.getAtom(0)
const tag = markup.getAtom(1)
// todo: add support for providing custom functions for inline markups?
// for example, !2+2! could run eval, or :about: could search a link map.
const attributes = markup.getAtomsFrom(2).join(" ")
markups = markups.filter(mu => mu.delimiter !== delimiter) // Remove any overridden markups
if (tag)
markups.push({delimiter, tag, attributes})
})
}
return markups
}
matchWholeLine = true
getMatches(text) {
const exclusives = []
return this.markups.map(markup => this.applyMarkup(text, markup, exclusives)).filter(i => i).flat()
}
applyMarkup(text, markup, exclusives = []) {
const {delimiter, tag, attributes} = markup
const escapedDelimiter = delimiter.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")
const pattern = new RegExp(`${escapedDelimiter}[^${escapedDelimiter}]+${escapedDelimiter}`, "g")
const delimiterLength = delimiter.length
return [...text.matchAll(pattern)].map(match => {
const { index } = match
const endIndex = index + match[0].length
// I'm too lazy to clean up sdk to write a proper inline markup parser so doing this for now.
// The exclusive idea is to not try and apply bold or italic styles inside a TeX or code inline style.
// Note that the way this is currently implemented any TeX in an inline code will get rendered, but code
// inline of TeX will not. Seems like an okay tradeoff until a proper refactor and cleanup can be done.
if (exclusives.some(exclusive => index >= exclusive[0] && index <= exclusive[1]))
return undefined
if (markup.exclusive)
exclusives.push([index, endIndex])
return [
{ index, string: `<${tag + (attributes ? " " + attributes : "")}>`, endIndex, consumeStartCharacters: delimiterLength },
{ index: endIndex, endIndex, string: `${tag}>`, consumeEndCharacters: delimiterLength }
]
}).filter(i => i)
}
}
class inlineMarkupParser extends inlineMarkupsOnParser {
get cueAtom() {
return this.getAtom(0)
}
get delimiterAtom() {
return this.getAtom(1)
}
get tagOrUrlAtom() {
return this.getAtom(2)
}
get htmlAttributesAtom() {
return this.getAtomsFrom(3)
}
get suggestInAutocomplete() { return false }
getMatches(text) {
try {
const delimiter = this.getAtom(1)
const tag = this.getAtom(2)
const attributes = this.getAtomsFrom(3).join(" ")
return this.applyMarkup(text, {delimiter, tag, attributes})
} catch (err) {
console.error(err)
return []
}
// Note: doubling up doesn't work because of the consumption characters.
}
}
class linkifyParser extends abstractAftertextDirectiveParser {
get cueAtom() {
return this.getAtom(0)
}
get booleanAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
}
class abstractMarkupParameterParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
}
class matchAllParser extends abstractMarkupParameterParser {
get suggestInAutocomplete() { return false }
}
class matchParser extends abstractMarkupParameterParser {
get integerAtom() {
return this.getAtomsFrom(0).map(val => parseInt(val))
}
get suggestInAutocomplete() { return false }
get indexes() {
return this.getAtomsFrom(1).map(num => parseInt(num))
}
}
class abstractHtmlAttributeParser extends ParserBackedParticle {
get suggestInAutocomplete() { return false }
buildHtml() {
return ""
}
}
class linkTargetParser extends abstractHtmlAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get anyAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
}
class blankLineParser extends ParserBackedParticle {
get blankAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
get isPopular() { return true }
buildHtml() {
return this.parent.clearSectionStack()
}
}
class chatLineParser extends ParserBackedParticle {
createParserCombinator() {
return new Particle.ParserCombinator(chatLineParser, undefined, undefined)
}
get anyAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class lineOfCodeParser extends ParserBackedParticle {
createParserCombinator() {
return new Particle.ParserCombinator(lineOfCodeParser, undefined, undefined)
}
get codeAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class commentLineParser extends ParserBackedParticle {
get commentAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class cssLineParser extends ParserBackedParticle {
createParserCombinator() {
return new Particle.ParserCombinator(cssLineParser, undefined, undefined)
}
get cssAnyAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class abstractTableTransformParser extends ParserBackedParticle {
createParserCombinator() {
return new Particle.ParserCombinator(undefined, Object.assign(Object.assign({}, super.createParserCombinator()._getCueMapAsObject()), {"#" : h1Parser,
"##" : h2Parser,
"?" : scrollQuestionParser,
"heatrix" : heatrixParser,
"heatrixAdvanced" : heatrixAdvancedParser,
"map" : mapParser,
"scatterplot" : scatterplotParser,
"sparkline" : sparklineParser,
"printColumn" : printColumnParser,
"printTable" : printTableParser,
"br" : scrollBrParser,
"splitYear" : scrollSplitYearParser,
"splitDayName" : scrollSplitDayNameParser,
"splitMonthName" : scrollSplitMonthNameParser,
"splitMonth" : scrollSplitMonthParser,
"splitDayOfMonth" : scrollSplitDayOfMonthParser,
"splitDay" : scrollSplitDayOfWeekParser,
"groupBy" : scrollGroupByParser,
"where" : scrollWhereParser,
"select" : scrollSelectParser,
"reverse" : scrollReverseParser,
"compose" : scrollComposeParser,
"compute" : scrollComputeParser,
"eval" : scrollEvalParser,
"rank" : scrollRankParser,
"links" : scrollLinksParser,
"limit" : scrollLimitParser,
"shuffle" : scrollShuffleParser,
"transpose" : scrollTransposeParser,
"impute" : scrollImputeParser,
"orderBy" : scrollOrderByParser,
"rename" : scrollRenameParser}), [{regex: /^, parser: htmlInlineParser}])
}
get cueAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
get coreTable() {
return this.parent.coreTable
}
get columnNames() {
return this.parent.columnNames
}
getRunTimeEnumOptions(atom) {
if (atom.atomTypeId === "columnNameAtom")
return this.parent.columnNames
return super.getRunTimeEnumOptions(atom)
}
getRunTimeEnumOptionsForValidation(atom) {
// Note: this will fail if the CSV file hasnt been built yet.
if (atom.atomTypeId === "columnNameAtom")
return this.parent.columnNames.concat(this.parent.columnNames.map(c => "-" + c)) // Add reverse names
return super.getRunTimeEnumOptions(atom)
}
}
class abstractDateSplitTransformParser extends abstractTableTransformParser {
get cueAtom() {
return this.getAtom(0)
}
get columnNameAtom() {
return this.getAtomsFrom(1)
}
get suggestInAutocomplete() { return false }
get coreTable() {
const columnName = this.getAtom(1) || this.detectDateColumn()
if (!columnName) return this.parent.coreTable
return this.parent.coreTable.map(row => {
const newRow = {...row}
try {
const date = this.root.dayjs(row[columnName])
if (date.isValid())
newRow[this.newColumnName] = this.transformDate(date)
} catch (err) {}
return newRow
})
}
detectDateColumn() {
const columns = this.parent.columnNames
const dateColumns = ['date', 'created', 'published', 'timestamp']
for (const col of dateColumns) {
if (columns.includes(col)) return col
}
for (const col of columns) {
const sample = this.parent.coreTable[0][col]
if (sample && this.root.dayjs(sample).isValid())
return col
}
return null
}
get columnNames() {
return [...this.parent.columnNames, this.newColumnName]
}
transformDate(date) {
const formatted = date.format(this.dateFormat)
const isInt = !this.cue.includes("Name")
return isInt ? parseInt(formatted) : formatted
}
}
class scrollSplitYearParser extends abstractDateSplitTransformParser {
get suggestInAutocomplete() { return false }
get dateFormat() { return `YYYY` }
get newColumnName() { return `year` }
}
class scrollSplitDayNameParser extends abstractDateSplitTransformParser {
get suggestInAutocomplete() { return false }
get dateFormat() { return `dddd` }
get newColumnName() { return `dayName` }
}
class scrollSplitMonthNameParser extends abstractDateSplitTransformParser {
get suggestInAutocomplete() { return false }
get dateFormat() { return `MMMM` }
get newColumnName() { return `monthName` }
}
class scrollSplitMonthParser extends abstractDateSplitTransformParser {
get suggestInAutocomplete() { return false }
get dateFormat() { return `M` }
get newColumnName() { return `month` }
}
class scrollSplitDayOfMonthParser extends abstractDateSplitTransformParser {
get suggestInAutocomplete() { return false }
get dateFormat() { return `D` }
get newColumnName() { return `dayOfMonth` }
}
class scrollSplitDayOfWeekParser extends abstractDateSplitTransformParser {
get suggestInAutocomplete() { return false }
get dateFormat() { return `d` }
get newColumnName() { return `day` }
}
class scrollGroupByParser extends abstractTableTransformParser {
get columnNameAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
get coreTable() {
if (this._coreTable) return this._coreTable
const groupByColNames = this.getAtomsFrom(1)
const {coreTable} = this.parent
if (!groupByColNames.length) return coreTable
const newCols = this.findParticles("reduce").map(reduceParticle => {
return {
source: reduceParticle.getAtom(1),
reduction: reduceParticle.getAtom(2),
name: reduceParticle.getAtom(3) || reduceParticle.getAtomsFrom(1).join("_")
}
})
// Pivot is shorthand for group and reduce?
const makePivotTable = (rows, groupByColumnNames, inputColumnNames, newCols) => {
const colMap = {}
inputColumnNames.forEach((col) => (colMap[col] = true))
const groupByCols = groupByColumnNames.filter((col) => colMap[col])
return new PivotTable(rows, inputColumnNames.map(c => {return {name: c}}), newCols).getNewRows(groupByCols)
}
class PivotTable {
constructor(rows, inputColumns, outputColumns) {
this._columns = {}
this._rows = rows
inputColumns.forEach((col) => (this._columns[col.name] = col))
outputColumns.forEach((col) => (this._columns[col.name] = col))
}
_getGroups(allRows, groupByColNames) {
const rowsInGroups = new Map()
allRows.forEach((row) => {
const groupKey = groupByColNames.map((col) => row[col]?.toString().replace(/ /g, "") || "").join(" ")
if (!rowsInGroups.has(groupKey)) rowsInGroups.set(groupKey, [])
rowsInGroups.get(groupKey).push(row)
})
return rowsInGroups
}
getNewRows(groupByCols) {
// make new particles
const rowsInGroups = this._getGroups(this._rows, groupByCols)
// Any column in the group should be reused by the children
const columns = [
{
name: "count",
type: "number",
min: 0,
},
]
groupByCols.forEach((colName) => columns.push(this._columns[colName]))
const colsToReduce = Object.values(this._columns).filter((col) => !!col.reduction)
colsToReduce.forEach((col) => columns.push(col))
// for each group
const rows = []
const totalGroups = rowsInGroups.size
for (let [groupId, group] of rowsInGroups) {
const firstRow = group[0]
const newRow = {}
groupByCols.forEach((col) =>
newRow[col] = firstRow ? firstRow[col] : 0
)
newRow.count = group.length
// todo: add more reductions? count, stddev, median, variance.
colsToReduce.forEach((col) => {
const sourceColName = col.source
const reduction = col.reduction
if (reduction === "concat") {
newRow[col.name] = group.map((row) => row[sourceColName]).join(" ")
return
}
if (reduction === "first") {
newRow[col.name] = group[0][sourceColName]
return
}
const values = group.map((row) => row[sourceColName]).filter((val) => typeof val === "number" && !isNaN(val))
let reducedValue = firstRow[sourceColName]
if (reduction === "sum") reducedValue = values.reduce((prev, current) => prev + current, 0)
if (reduction === "max") reducedValue = Math.max(...values)
if (reduction === "min") reducedValue = Math.min(...values)
if (reduction === "mean") reducedValue = values.reduce((prev, current) => prev + current, 0) / values.length
newRow[col.name] = reducedValue
})
rows.push(newRow)
}
// todo: add tests. figure out this api better.
Object.values(columns).forEach((col) => {
// For pivot columns, remove the source and reduction info for now. Treat things as immutable.
delete col.source
delete col.reduction
})
return {
rows,
columns,
}
}
}
const pivotTable = makePivotTable(coreTable, groupByColNames, this.parent.columnNames, newCols)
this._coreTable = pivotTable.rows
this._columnNames = pivotTable.columns.map(col => col.name)
return pivotTable.rows
}
get columnNames() {
const {coreTable} = this
return this._columnNames || this.parent.columnNames
}
}
class scrollWhereParser extends abstractTableTransformParser {
get cueAtom() {
return this.getAtom(0)
}
get columnNameAtom() {
return this.getAtom(1)
}
get comparisonAtom() {
return this.getAtom(2)
}
get atomAtom() {
return this.getAtom(3)
}
get suggestInAutocomplete() { return false }
get coreTable() {
// todo: use atoms here.
const columnName = this.getAtom(1)
const operator = this.getAtom(2)
let untypedScalarValue = this.getAtom(3)
const typedValue = isNaN(parseFloat(untypedScalarValue)) ? untypedScalarValue : parseFloat(untypedScalarValue)
const coreTable = this.parent.coreTable
if (!columnName || !operator || untypedScalarValue === undefined) return coreTable
const filterFn = row => {
const atom = row[columnName]
const typedAtom = atom === null ? undefined : atom // convert nulls to undefined
if (operator === "=") return typedValue === typedAtom
else if (operator === "!=") return typedValue !== typedAtom
else if (operator === "includes") return typedAtom !== undefined && typedAtom.includes(typedValue)
else if (operator === "startsWith") return typedAtom !== undefined && typedAtom.toString().startsWith(typedValue)
else if (operator === "endsWith") return typedAtom !== undefined && typedAtom.toString().endsWith(typedValue)
else if (operator === "doesNotInclude") return typedAtom === undefined || !typedAtom.includes(typedValue)
else if (operator === ">") return typedAtom > typedValue
else if (operator === "<") return typedAtom < typedValue
else if (operator === ">=") return typedAtom >= typedValue
else if (operator === "<=") return typedAtom <= typedValue
else if (operator === "empty") return atom === "" || atom === undefined
else if (operator === "notEmpty") return !(atom === "" || atom === undefined)
}
return coreTable.filter(filterFn)
}
}
class scrollSelectParser extends abstractTableTransformParser {
get columnNameAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
get coreTable() {
const {coreTable} = this.parent
const {columnNames} = this
if (!columnNames.length) return coreTable
return coreTable.map(row => Object.fromEntries(columnNames.map(colName => [colName, row[colName]])))
}
get columnNames() {
return this.getAtomsFrom(1)
}
}
class scrollReverseParser extends abstractTableTransformParser {
get suggestInAutocomplete() { return false }
get coreTable() {
return this.parent.coreTable.slice().reverse()
}
}
class scrollComposeParser extends abstractTableTransformParser {
get stringAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
get coreTable() {
const {newColumnName} = this
const formatString = this.getAtomsFrom(2).join(" ")
return this.parent.coreTable.map((row, index) => {
const newRow = Object.assign({}, row)
newRow[newColumnName] = this.evaluate(new Particle(row).evalTemplateString(formatString), index)
return newRow
})
}
evaluate(str) {
return str
}
get newColumnName() {
return this.atoms[1]
}
get columnNames() {
return this.parent.columnNames.concat(this.newColumnName)
}
}
class scrollComputeParser extends scrollComposeParser {
get stringAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
evaluate(str) {
return parseFloat(eval(str))
}
}
class scrollEvalParser extends scrollComputeParser {
get suggestInAutocomplete() { return false }
evaluate(str) {
return eval(str)
}
}
class scrollRankParser extends scrollComposeParser {
get suggestInAutocomplete() { return false }
get newColumnName() { return `rank` }
evaluate(str, index) { return index + 1 }
}
class scrollLinksParser extends abstractTableTransformParser {
get columnNameAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
get coreTable() {
const {newColumnName, linkColumns} = this
return this.parent.coreTable.map(row => {
const newRow = Object.assign({}, row)
let newValue = []
linkColumns.forEach(name => {
const value = newRow[name]
delete newRow[name]
if (value) newValue.push(`${name}`)
})
newRow[newColumnName] = newValue.join(" ")
return newRow
})
}
get newColumnName() {
return "links"
}
get linkColumns() {
return this.getAtomsFrom(1)
}
get columnNames() {
const {linkColumns} = this
return this.parent.columnNames.filter(name => !linkColumns.includes(name)).concat(this.newColumnName)
}
}
class scrollLimitParser extends abstractTableTransformParser {
get cueAtom() {
return this.getAtom(0)
}
get integerAtom() {
return parseInt(this.getAtom(1))
}
get integerAtom() {
return parseInt(this.getAtom(2))
}
get suggestInAutocomplete() { return false }
get coreTable() {
let start = this.getAtom(1)
let end = this.getAtom(2)
if (end === undefined) {
end = start
start = 0
}
return this.parent.coreTable.slice(parseInt(start), parseInt(end))
}
}
class scrollShuffleParser extends abstractTableTransformParser {
get suggestInAutocomplete() { return false }
get coreTable() {
// Create a copy of the table to avoid modifying original
const rows = this.parent.coreTable.slice()
// Fisher-Yates shuffle algorithm
for (let i = rows.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[rows[i], rows[j]] = [rows[j], rows[i]]
}
return rows
}
}
class scrollTransposeParser extends abstractTableTransformParser {
get suggestInAutocomplete() { return false }
get coreTable() {
// todo: we need to switch to column based coreTable, instead of row based
const transpose = arr => Object.keys(arr[0]).map(key => [key, ...arr.map(row => row[key])]);
return transpose(this.parent.coreTable)
}
}
class scrollImputeParser extends abstractTableTransformParser {
get cueAtom() {
return this.getAtom(0)
}
get columnNameAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
get coreTable() {
const {columnName} = this
const sorted = this.root.lodash.orderBy(this.parent.coreTable.slice(), columnName)
// ascending
const imputed = []
let lastInserted = sorted[0][columnName]
sorted.forEach(row => {
const measuredTime = row[columnName]
while (measuredTime > lastInserted + 1) {
lastInserted++
// synthesize rows
const imputedRow = {}
imputedRow[columnName] = lastInserted
imputedRow.count = 0
imputed.push(imputedRow)
}
lastInserted = measuredTime
imputed.push(row)
})
return imputed
}
get columnName() {
return this.getAtom(1)
}
}
class scrollOrderByParser extends abstractTableTransformParser {
get columnNameAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
get coreTable() {
const makeLodashOrderByParams = str => {
const part1 = str.split(" ")
const part2 = part1.map(col => (col.startsWith("-") ? "desc" : "asc"))
return [part1.map(col => col.replace(/^\-/, "")), part2]
}
const orderBy = makeLodashOrderByParams(this.content)
return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])
}
}
class scrollRenameParser extends abstractTableTransformParser {
get atomAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
get coreTable() {
const {coreTable} = this.parent
const {renameMap} = this
if (!Object.keys(renameMap).length) return coreTable
return coreTable.map(row => {
const newRow = {}
Object.keys(row).forEach(key => {
const name = renameMap[key] || key
newRow[name] = row[key]
})
return newRow
})
}
get renameMap() {
const map = {}
const pairs = this.getAtomsFrom(1)
let oldName
while (oldName = pairs.shift()) {
map[oldName] = pairs.shift()
}
return map
}
_renamed
get columnNames() {
if (this._renamed)
return this._renamed
const {renameMap} = this
this._renamed = this.parent.columnNames.map(name => renameMap[name] || name )
return this._renamed
}
}
class errorParser extends ParserBackedParticle {
getErrors() { return this._getErrorParserErrors() }
get suggestInAutocomplete() { return false }
}
class hakonContentParser extends ParserBackedParticle {
get anyAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class heatrixCatchAllParser extends ParserBackedParticle {
get stringAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class lineOfTextParser extends ParserBackedParticle {
get stringAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
get isTextParser() { return true }
}
class htmlLineParser extends ParserBackedParticle {
createParserCombinator() {
return new Particle.ParserCombinator(htmlLineParser, undefined, undefined)
}
get htmlAnyAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class openGraphParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
}
class scrollFooterParser extends ParserBackedParticle {
get preBuildCommandAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
}
class scriptLineParser extends ParserBackedParticle {
createParserCombinator() {
return new Particle.ParserCombinator(scriptLineParser, undefined, undefined)
}
get scriptAnyAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class linkTitleParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get anyAtom() {
return this.getAtomsFrom(1)
}
get suggestInAutocomplete() { return false }
}
class programLinkParser extends ParserBackedParticle {
get codeAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class scrollMediaLoopParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
}
class scrollAutoplayParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
}
class abstractCompilerRuleParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get anyAtom() {
return this.getAtomsFrom(1)
}
get suggestInAutocomplete() { return false }
}
class closeSubparticlesParser extends abstractCompilerRuleParser {
get suggestInAutocomplete() { return false }
}
class indentCharacterParser extends abstractCompilerRuleParser {
get suggestInAutocomplete() { return false }
}
class catchAllAtomDelimiterParser extends abstractCompilerRuleParser {
get suggestInAutocomplete() { return false }
}
class openSubparticlesParser extends abstractCompilerRuleParser {
get suggestInAutocomplete() { return false }
}
class stringTemplateParser extends abstractCompilerRuleParser {
get suggestInAutocomplete() { return false }
}
class joinSubparticlesWithParser extends abstractCompilerRuleParser {
get suggestInAutocomplete() { return false }
}
class abstractConstantParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
}
class parsersBooleanParser extends abstractConstantParser {
get cueAtom() {
return this.getAtom(0)
}
get constantIdentifierAtom() {
return this.getAtom(1)
}
get booleanAtom() {
return this.getAtomsFrom(2)
}
get suggestInAutocomplete() { return false }
}
class parsersFloatParser extends abstractConstantParser {
get cueAtom() {
return this.getAtom(0)
}
get constantIdentifierAtom() {
return this.getAtom(1)
}
get floatAtom() {
return this.getAtomsFrom(2).map(val => parseFloat(val))
}
get suggestInAutocomplete() { return false }
}
class parsersIntParser extends abstractConstantParser {
get cueAtom() {
return this.getAtom(0)
}
get constantIdentifierAtom() {
return this.getAtom(1)
}
get integerAtom() {
return this.getAtomsFrom(2).map(val => parseInt(val))
}
get suggestInAutocomplete() { return false }
}
class parsersStringParser extends abstractConstantParser {
createParserCombinator() {
return new Particle.ParserCombinator(catchAllMultilineStringConstantParser, undefined, undefined)
}
get cueAtom() {
return this.getAtom(0)
}
get constantIdentifierAtom() {
return this.getAtom(1)
}
get stringAtom() {
return this.getAtomsFrom(2)
}
get suggestInAutocomplete() { return false }
}
class abstractParserRuleParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
}
class abstractNonTerminalParserRuleParser extends abstractParserRuleParser {
get suggestInAutocomplete() { return false }
}
class parsersBaseParserParser extends abstractParserRuleParser {
get cueAtom() {
return this.getAtom(0)
}
get baseParsersAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
}
class catchAllAtomTypeParser extends abstractParserRuleParser {
get cueAtom() {
return this.getAtom(0)
}
get atomTypeIdAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
}
class atomParserParser extends abstractParserRuleParser {
get cueAtom() {
return this.getAtom(0)
}
get atomParserAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
}
class catchAllParserParser extends abstractParserRuleParser {
get cueAtom() {
return this.getAtom(0)
}
get parserIdAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
}
class parsersAtomsParser extends abstractParserRuleParser {
get atomTypeIdAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class parsersCompilerParser extends abstractParserRuleParser {
createParserCombinator() {
return new Particle.ParserCombinator(undefined, Object.assign(Object.assign({}, super.createParserCombinator()._getCueMapAsObject()), {"closeSubparticles" : closeSubparticlesParser,
"indentCharacter" : indentCharacterParser,
"catchAllAtomDelimiter" : catchAllAtomDelimiterParser,
"openSubparticles" : openSubparticlesParser,
"stringTemplate" : stringTemplateParser,
"joinSubparticlesWith" : joinSubparticlesWithParser}), undefined)
}
get suggestInAutocomplete() { return false }
}
class parserDescriptionParser extends abstractParserRuleParser {
get stringAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class parsersExampleParser extends abstractParserRuleParser {
createParserCombinator() {
return new Particle.ParserCombinator(catchAllExampleLineParser, undefined, undefined)
}
get exampleAnyAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class extendsParserParser extends abstractParserRuleParser {
get cueAtom() {
return this.getAtom(0)
}
get parserIdAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
}
class parsersPopularityParser extends abstractParserRuleParser {
get cueAtom() {
return this.getAtom(0)
}
get floatAtom() {
return parseFloat(this.getAtom(1))
}
get suggestInAutocomplete() { return false }
}
class inScopeParser extends abstractParserRuleParser {
get parserIdAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class parsersJavascriptParser extends abstractParserRuleParser {
createParserCombinator() {
return new Particle.ParserCombinator(catchAllJavascriptCodeLineParser, undefined, undefined)
}
get suggestInAutocomplete() { return false }
format() {
if (this.isNodeJs()) {
const template = `class FOO{ ${this.subparticlesToString()}}`
this.setSubparticles(
require("prettier")
.format(template, { semi: false, useTabs: true, parser: "babel", printWidth: 240 })
.replace(/class FOO \{\s+/, "")
.replace(/\s+\}\s+$/, "")
.replace(/\n\t/g, "\n") // drop one level of indent
.replace(/\t/g, " ") // we used tabs instead of spaces to be able to dedent without breaking literals.
)
}
return this
}
}
class abstractParseRuleParser extends abstractParserRuleParser {
get suggestInAutocomplete() { return false }
}
class parsersCueParser extends abstractParseRuleParser {
get cueAtom() {
return this.getAtom(0)
}
get stringAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
}
class cueFromIdParser extends abstractParseRuleParser {
get cueAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
}
class parsersPatternParser extends abstractParseRuleParser {
get regexAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class parsersRequiredParser extends abstractParserRuleParser {
get suggestInAutocomplete() { return false }
}
class abstractValidationRuleParser extends abstractParserRuleParser {
get booleanAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class parsersSingleParser extends abstractValidationRuleParser {
get suggestInAutocomplete() { return false }
}
class uniqueLineParser extends abstractValidationRuleParser {
get suggestInAutocomplete() { return false }
}
class uniqueCueParser extends abstractValidationRuleParser {
get suggestInAutocomplete() { return false }
}
class listDelimiterParser extends abstractParserRuleParser {
get stringAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class contentKeyParser extends abstractParserRuleParser {
get stringAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class subparticlesKeyParser extends abstractParserRuleParser {
get stringAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class parsersTagsParser extends abstractParserRuleParser {
get stringAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class atomTypeDescriptionParser extends ParserBackedParticle {
get stringAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class catchAllErrorParser extends ParserBackedParticle {
getErrors() { return this._getErrorParserErrors() }
get suggestInAutocomplete() { return false }
}
class catchAllExampleLineParser extends ParserBackedParticle {
createParserCombinator() {
return new Particle.ParserCombinator(catchAllExampleLineParser, undefined, undefined)
}
get exampleAnyAtom() {
return this.getAtom(0)
}
get exampleAnyAtom() {
return this.getAtomsFrom(1)
}
get suggestInAutocomplete() { return false }
}
class catchAllJavascriptCodeLineParser extends ParserBackedParticle {
createParserCombinator() {
return new Particle.ParserCombinator(catchAllJavascriptCodeLineParser, undefined, undefined)
}
get javascriptCodeAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class catchAllMultilineStringConstantParser extends ParserBackedParticle {
createParserCombinator() {
return new Particle.ParserCombinator(catchAllMultilineStringConstantParser, undefined, undefined)
}
get stringAtom() {
return this.getAtom(0)
}
get stringAtom() {
return this.getAtomsFrom(1)
}
get suggestInAutocomplete() { return false }
}
class atomTypeDefinitionParser extends ParserBackedParticle {
createParserCombinator() {
return new Particle.ParserCombinator(undefined, Object.assign(Object.assign({}, super.createParserCombinator()._getCueMapAsObject()), {"//" : slashCommentParser,
"description" : atomTypeDescriptionParser,
"enumFromAtomTypes" : enumFromAtomTypesParser,
"enum" : parsersEnumParser,
"examples" : parsersExamplesParser,
"min" : atomMinParser,
"max" : atomMaxParser,
"paint" : parsersPaintParser,
"regex" : parsersRegexParser,
"reservedAtoms" : reservedAtomsParser,
"extends" : extendsAtomTypeParser}), undefined)
}
get atomTypeIdAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
buildHtml() {return ""}
}
class enumFromAtomTypesParser extends ParserBackedParticle {
get atomPropertyNameAtom() {
return this.getAtom(0)
}
get atomTypeIdAtom() {
return this.getAtomsFrom(1)
}
get suggestInAutocomplete() { return false }
}
class parsersEnumParser extends ParserBackedParticle {
get atomPropertyNameAtom() {
return this.getAtom(0)
}
get enumOptionAtom() {
return this.getAtomsFrom(1)
}
get suggestInAutocomplete() { return false }
}
class parsersExamplesParser extends ParserBackedParticle {
get atomPropertyNameAtom() {
return this.getAtom(0)
}
get atomExampleAtom() {
return this.getAtomsFrom(1)
}
get suggestInAutocomplete() { return false }
}
class atomMinParser extends ParserBackedParticle {
get atomPropertyNameAtom() {
return this.getAtom(0)
}
get numberAtom() {
return parseFloat(this.getAtom(1))
}
get suggestInAutocomplete() { return false }
}
class atomMaxParser extends ParserBackedParticle {
get atomPropertyNameAtom() {
return this.getAtom(0)
}
get numberAtom() {
return parseFloat(this.getAtom(1))
}
get suggestInAutocomplete() { return false }
}
class parsersPaintParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get paintTypeAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
}
class parserDefinitionParser extends ParserBackedParticle {
createParserCombinator() {
return new Particle.ParserCombinator(catchAllErrorParser, Object.assign(Object.assign({}, super.createParserCombinator()._getCueMapAsObject()), {"//" : slashCommentParser,
"boolean" : parsersBooleanParser,
"float" : parsersFloatParser,
"int" : parsersIntParser,
"string" : parsersStringParser,
"baseParser" : parsersBaseParserParser,
"catchAllAtomType" : catchAllAtomTypeParser,
"atomParser" : atomParserParser,
"catchAllParser" : catchAllParserParser,
"atoms" : parsersAtomsParser,
"compiler" : parsersCompilerParser,
"description" : parserDescriptionParser,
"example" : parsersExampleParser,
"extends" : extendsParserParser,
"popularity" : parsersPopularityParser,
"inScope" : inScopeParser,
"javascript" : parsersJavascriptParser,
"cue" : parsersCueParser,
"cueFromId" : cueFromIdParser,
"pattern" : parsersPatternParser,
"required" : parsersRequiredParser,
"single" : parsersSingleParser,
"uniqueLine" : uniqueLineParser,
"uniqueCue" : uniqueCueParser,
"listDelimiter" : listDelimiterParser,
"contentKey" : contentKeyParser,
"subparticlesKey" : subparticlesKeyParser,
"tags" : parsersTagsParser}), [{regex: /^[a-zA-Z0-9_]+Parser$/, parser: parserDefinitionParser}])
}
get parserIdAtom() {
return this.getAtom(0)
}
get suggestInAutocomplete() { return false }
buildHtml() { return ""}
}
class parsersRegexParser extends ParserBackedParticle {
get atomPropertyNameAtom() {
return this.getAtom(0)
}
get regexAtom() {
return this.getAtomsFrom(1)
}
get suggestInAutocomplete() { return false }
}
class reservedAtomsParser extends ParserBackedParticle {
get atomPropertyNameAtom() {
return this.getAtom(0)
}
get reservedAtomAtom() {
return this.getAtomsFrom(1)
}
get suggestInAutocomplete() { return false }
}
class extendsAtomTypeParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get atomTypeIdAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
}
class abstractColumnNameParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get columnNameAtom() {
return this.getAtom(1)
}
get suggestInAutocomplete() { return false }
getRunTimeEnumOptions(atom) {
if (atom.atomTypeId === "columnNameAtom")
return this.parent.columnNames
return super.getRunTimeEnumOptions(atom)
}
}
class scrollRadiusParser extends abstractColumnNameParser {
get suggestInAutocomplete() { return false }
}
class scrollSymbolParser extends abstractColumnNameParser {
get suggestInAutocomplete() { return false }
}
class scrollFillParser extends abstractColumnNameParser {
get suggestInAutocomplete() { return false }
}
class scrollLabelParser extends abstractColumnNameParser {
get suggestInAutocomplete() { return false }
}
class scrollXParser extends abstractColumnNameParser {
get suggestInAutocomplete() { return false }
}
class scrollYParser extends abstractColumnNameParser {
get suggestInAutocomplete() { return false }
}
class quoteLineParser extends ParserBackedParticle {
createParserCombinator() {
return new Particle.ParserCombinator(quoteLineParser, undefined, undefined)
}
get anyAtom() {
return this.getAtomsFrom(0)
}
get suggestInAutocomplete() { return false }
}
class scrollParser extends ParserBackedParticle {
createParserCombinator() {
return new Particle.ParserCombinator(catchAllParagraphParser, Object.assign(Object.assign({}, super.createParserCombinator()._getCueMapAsObject()), {"scrollParagraph" : scrollParagraphParser,
"authors" : authorsParser,
"blink" : blinkParser,
"button" : scrollButtonParser,
"catchAllParagraph" : catchAllParagraphParser,
"center" : scrollCenterParser,
"[]" : checklistTodoParser,
"[x]" : checklistDoneParser,
"-" : listAftertextParser,
">" : quickQuoteParser,
"counter" : scrollCounterParser,
"expander" : expanderParser,
"#" : h1Parser,
"##" : h2Parser,
"###" : h3Parser,
"####" : h4Parser,
"?" : scrollQuestionParser,
"#####" : h5Parser,
"printTitle" : printTitleParser,
"caption" : captionAftertextParser,
"music" : scrollMusicParser,
"video" : scrollVideoParser,
"*" : quickParagraphParser,
"stopwatch" : scrollStopwatchParser,
"thinColumns" : thinColumnsParser,
"wideColumns" : wideColumnsParser,
"wideColumn" : wideColumnParser,
"mediumColumns" : mediumColumnsParser,
"mediumColumn" : mediumColumnParser,
"thinColumn" : thinColumnParser,
"endColumns" : endColumnsParser,
"container" : scrollContainerParser,
"---" : horizontalRuleParser,
"***" : scrollDinkusParser,
"dinkus" : customDinkusParser,
"****" : endOfPostDinkusParser,
"downloadButton" : downloadButtonParser,
"editButton" : editButtonParser,
"emailButton" : emailButtonParser,
"homeButton" : homeButtonParser,
"theScrollButton" : theScrollButtonParser,
"editLink" : editLinkParser,
"scrollVersionLink" : scrollVersionLinkParser,
"classicForm" : classicFormParser,
"scrollForm" : scrollFormParser,
"loremIpsum" : loremIpsumParser,
"nickelbackIpsum" : nickelbackIpsumParser,
"printSnippets" : printSnippetsParser,
"nav" : scrollNavParser,
"printFullSnippets" : printFullSnippetsParser,
"printShortSnippets" : printShortSnippetsParser,
"printRelated" : printRelatedParser,
"notices" : scrollNoticesParser,
"printSourceStack" : printSourceStackParser,
"assertHtmlEquals" : assertHtmlEqualsParser,
"assertBuildIncludes" : assertBuildIncludesParser,
"assertHtmlIncludes" : assertHtmlIncludesParser,
"assertHtmlExcludes" : assertHtmlExcludesParser,
"printAuthors" : printAuthorsParser,
"printDate" : printDateParser,
"printFormatLinks" : printFormatLinksParser,
"buildCsv" : buildCsvParser,
"buildTsv" : buildTsvParser,
"buildJson" : buildJsonParser,
"buildCss" : buildCssParser,
"buildHtml" : buildHtmlParser,
"buildJs" : buildJsParser,
"buildRss" : buildRssParser,
"buildTxt" : buildTxtParser,
"loadConcepts" : loadConceptsParser,
"buildConcepts" : buildConceptsParser,
"buildParsers" : buildParsersParser,
"fetch" : fetchParser,
"buildMeasures" : buildMeasuresParser,
"buildPdf" : buildPdfParser,
"testStrict" : testStrictParser,
"date" : scrollDateParser,
"editBaseUrl" : editBaseUrlParser,
"canonicalUrl" : canonicalUrlParser,
"openGraphImage" : openGraphImageParser,
"baseUrl" : baseUrlParser,
"rssFeedUrl" : rssFeedUrlParser,
"editUrl" : editUrlParser,
"email" : siteOwnerEmailParser,
"favicon" : faviconParser,
"importOnly" : importOnlyParser,
"inlineMarkups" : inlineMarkupsParser,
"htmlLang" : htmlLangParser,
"description" : openGraphDescriptionParser,
"permalink" : permalinkParser,
"tags" : scrollTagsParser,
"title" : scrollTitleParser,
"linkTitle" : scrollLinkTitleParser,
"chat" : scrollChatParser,
"table" : scrollTableParser,
"cloc" : clocParser,
"dependencies" : scrollDependenciesParser,
"disk" : scrollDiskParser,
"iris" : scrollIrisParser,
"concepts" : scrollConceptsParser,
"posts" : scrollPostsParser,
"postsMeta" : scrollPostsMetaParser,
"printFeed" : printFeedParser,
"printSource" : printSourceParser,
"printSiteMap" : printSiteMapParser,
"code" : codeParser,
"codeWithHeader" : codeWithHeaderParser,
"codeFromFile" : codeFromFileParser,
"copyButtons" : copyButtonsParser,
"heatrix" : heatrixParser,
"heatrixAdvanced" : heatrixAdvancedParser,
"map" : mapParser,
"scatterplot" : scatterplotParser,
"sparkline" : sparklineParser,
"printColumn" : printColumnParser,
"printTable" : printTableParser,
"katex" : katexParser,
"helpfulNotFound" : helpfulNotFoundParser,
"slideshow" : slideshowParser,
"tableSearch" : tableSearchParser,
"comment" : commentParser,
"!" : counterpointParser,
"//" : slashCommentParser,
"thanksTo" : thanksToParser,
"clearStack" : scrollClearStackParser,
"css" : cssParser,
"inlineCss" : inlineCssParser,
"background" : scrollBackgroundColorParser,
"color" : scrollFontColorParser,
"font" : scrollFontParser,
"dashboard" : scrollDashboardParser,
"belowAsCode" : belowAsCodeParser,
"belowAsCodeUntil" : belowAsCodeUntilParser,
"aboveAsCode" : aboveAsCodeParser,
"inspectBelow" : inspectBelowParser,
"inspectAbove" : inspectAboveParser,
"hakon" : hakonParser,
"html" : htmlParser,
"br" : scrollBrParser,
"iframes" : iframesParser,
"image" : scrollImageParser,
"qrcode" : qrcodeParser,
"youtube" : youtubeParser,
"youTube" : youTubeParser,
"import" : importParser,
"imported" : scrollImportedParser,
"inlineJs" : inlineJsParser,
"script" : scriptParser,
"jsonScript" : jsonScriptParser,
"leftRightButtons" : scrollLeftRightButtonsParser,
"keyboardNav" : keyboardNavParser,
"printUsageStats" : printUsageStatsParser,
"printScrollLeetSheet" : printScrollLeetSheetParser,
"printparsersLeetSheet" : printparsersLeetSheetParser,
"metaTags" : metaTagsParser,
"quote" : quoteParser,
"redirectTo" : redirectToParser,
"replace" : replaceParser,
"replaceJs" : replaceJsParser,
"replaceNodejs" : replaceNodejsParser,
"run" : runScriptParser,
"endSnippet" : endSnippetParser,
"toStamp" : toStampParser,
"stamp" : stampParser,
"stump" : scrollStumpParser,
"stumpNoSnippet" : stumpNoSnippetParser,
"plainText" : plainTextParser,
"plainTextOnly" : plainTextOnlyParser,
"theme" : scrollThemeParser}), [{regex: /^\d+\. /, parser: orderedListAftertextParser},{regex: /^\^.+$/, parser: footnoteDefinitionParser},{regex: /^[^\s]+\.(mp3|wav|ogg|aac|m4a|flac)/, parser: quickSoundParser},{regex: /^[^\s]+\.(mp4|webm|avi|mov)/, parser: quickVideoParser},{regex: /^[^\s]+\.(tsv|csv|ssv|psv|json)[^\s]*$/, parser: quickTableParser},{regex: /^[a-zA-Z0-9_]+Code$/, parser: codeWithLanguageParser},{regex: /^[^\s]+\.(css)$/, parser: quickCssParser},{regex: /^[^\s]+\.(html|htm)$/, parser: quickIncludeHtmlParser},{regex: /^[^\s]+\.(js)$/, parser: quickScriptParser},{regex: /^[a-zA-Z0-9_]+Def/, parser: scrollDefParser},{regex: /^%?[\w\.]+#[\w\.]+ */, parser: hamlParser},{regex: /^%[^#]+$/, parser: hamlTagParser},{regex: /^, parser: htmlInlineParser},{regex: /^[^\s]+\.(jpg|jpeg|png|gif|webp|svg|bmp)/, parser: quickImageParser},{regex: /^[^\s]+\.(scroll|parsers)$/, parser: quickImportParser},{regex: /^[^\s]+\.(py|pl|sh|rb|php)[^\s]*$/, parser: quickRunScriptParser},{regex: /^$/, parser: blankLineParser},{regex: /^[a-zA-Z0-9_]+Atom$/, parser: atomTypeDefinitionParser},{regex: /^[a-zA-Z0-9_]+Parser$/, parser: parserDefinitionParser}])
}
get suggestInAutocomplete() { return false }
setFile(file) {
this.file = file
const date = this.get("date")
if (date) this.file.timestamp = this.dayjs(this.get("date")).unix()
return this
}
buildHtml(buildSettings) {
this.sectionStack = []
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()
}
sectionStack = []
clearSectionStack() {
const result = this.sectionStack.join("\n")
this.sectionStack = []
return result
}
bodyStack = []
clearBodyStack() {
const result = this.bodyStack.join("")
this.bodyStack = []
return result
}
get hakonParser() {
if (this.isNodeJs())
return require("scrollsdk/products/hakon.nodejs.js")
return hakonParser
}
readSyncFromFileOrUrl(fileOrUrl) {
if (!this.isNodeJs()) return localStorage.getItem(fileOrUrl) || ""
const isUrl = fileOrUrl.match(/^https?\:[^ ]+$/)
if (!isUrl) return this.root.readFile(fileOrUrl)
return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))
}
async fetch(url, filename) {
const isUrl = url.match(/^https?\:[^ ]+$/)
if (!isUrl) return
return this.isNodeJs() ? this.fetchNode(url, filename) : this.fetchBrowser(url)
}
get path() {
return require("path")
}
makeFullPath(filename) {
return this.path.join(this.folderPath, filename)
}
_nextAndPrevious(arr, index) {
const nextIndex = index + 1
const previousIndex = index - 1
return {
previous: arr[previousIndex] ?? arr[arr.length - 1],
next: arr[nextIndex] ?? arr[0]
}
}
// keyboard nav is always in the same folder. does not currently support cross folder
includeFileInKeyboardNav(file) {
const { scrollProgram } = file
return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag)
}
get timeIndex() {
return this.file.timeIndex || 0
}
get linkToPrevious() {
if (!this.hasKeyboardNav)
// Dont provide link to next unless keyboard nav is on
return undefined
const {allScrollFiles} = this
let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous
while (!this.includeFileInKeyboardNav(file)) {
file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous
}
return file.scrollProgram.permalink
}
importRegex = /^(import |[a-zA-Z\_\-\.0-9\/]+\.(scroll|parsers)$|https?:\/\/.+\.(scroll|parsers)$)/gm
get linkToNext() {
if (!this.hasKeyboardNav)
// Dont provide link to next unless keyboard nav is on
return undefined
const {allScrollFiles} = this
let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next
while (!this.includeFileInKeyboardNav(file)) {
file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next
}
return file.scrollProgram.permalink
}
// todo: clean up this naming pattern and add a parser instead of special casing 404.html
get allHtmlFiles() {
return this.allScrollFiles.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.permalink !== "404.html")
}
parseNestedTag(tag) {
if (!tag.includes("/")) return;
const {path} = this
const parts = tag.split("/")
const group = parts.pop()
const relativePath = parts.join("/")
return {
group,
relativePath,
folderPath: path.join(this.folderPath, path.normalize(relativePath))
}
}
getFilesByTags(tags, limit) {
// todo: tags is currently matching partial substrings
const getFilesWithTag = (tag, files) => files.filter(file => file.scrollProgram.buildsHtml && file.scrollProgram.tags.includes(tag))
if (typeof tags === "string") tags = tags.split(" ")
if (!tags || !tags.length)
return this.allHtmlFiles
.filter(file => file !== this) // avoid infinite loops. todo: think this through better.
.map(file => {
return { file, relativePath: "" }
})
.slice(0, limit)
let arr = []
tags.forEach(tag => {
if (!tag.includes("/"))
return (arr = arr.concat(
getFilesWithTag(tag, this.allScrollFiles)
.map(file => {
return { file, relativePath: "" }
})
.slice(0, limit)
))
const {folderPath, group, relativePath} = this.parseNestedTag(tag)
let files = []
try {
files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this)
} catch (err) {
console.error(err)
}
const filtered = getFilesWithTag(group, files).map(file => {
return { file, relativePath: relativePath + "/" }
})
arr = arr.concat(filtered.slice(0, limit))
})
return this.lodash.sortBy(arr, file => file.file.timestamp).reverse()
}
async fetchNode(url, filename) {
filename = filename || new URL(url).pathname.split('/').pop()
const fullpath = this.makeFullPath(filename)
if (require("fs").existsSync(fullpath)) return this.readFile(fullpath)
this.log(`🛜 fetching ${url} to ${fullpath} `)
await this.downloadToDisk(url, fullpath)
return this.readFile(fullpath)
}
log(message) {
if (this.logger) this.logger.log(message)
}
async fetchBrowser(url) {
const content = localStorage.getItem(url)
if (content) return content
return this.downloadToLocalStorage(url)
}
async downloadToDisk(url, destination) {
const { writeFile } = require('fs').promises
const response = await fetch(url)
const fileBuffer = await response.arrayBuffer()
await writeFile(destination, Buffer.from(fileBuffer))
return this.readFile(destination)
}
async downloadToLocalStorage(url) {
const response = await fetch(url)
const blob = await response.blob()
localStorage.setItem(url, await blob.text())
return localStorage.getItem(url)
}
readFile(filename) {
const {path} = this
const fs = require("fs")
const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, ""))
if (fs.existsSync(fullPath))
return fs.readFileSync(fullPath, "utf8")
console.error(`File '${filename}' not found`)
return ""
}
alreadyRequired = new Set()
buildHtmlSnippet(buildSettings) {
this.sectionStack = []
return this.map(subparticle => (subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))
.filter(i => i)
.join("\n")
.trim() + this.clearSectionStack()
}
get footnotes() {
if (this._footnotes === undefined) this._footnotes = this.filter(particle => particle.isFootnote)
return this._footnotes
}
get authors() {
return this.get("authors")
}
get allScrollFiles() {
try {
return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this)
} catch (err) {
console.error(err)
return []
}
}
async doThing(thing) {
await Promise.all(this.filter(particle => particle[thing]).map(async particle => particle[thing]()))
}
async load() {
await this.doThing("load")
}
async execute() {
await this.doThing("execute")
}
file = {}
getFromParserId(parserId) {
return this.parserIdIndex[parserId]?.[0].content
}
get fileSystem() {
return this.file.fileSystem
}
get filePath() {
return this.file.filePath
}
get folderPath() {
return this.file.folderPath
}
get filename() {
return this.file.filename || ""
}
get hasKeyboardNav() {
return this.has("keyboardNav")
}
get editHtml() {
return `Edit`
}
get externalsPath() {
return this.file.EXTERNALS_PATH
}
get endSnippetIndex() {
// Get the line number that the snippet should stop at.
// First if its hard coded, use that
if (this.has("endSnippet")) return this.getParticle("endSnippet").index
// Next look for a dinkus
const snippetBreak = this.find(particle => particle.isDinkus)
if (snippetBreak) return snippetBreak.index
return -1
}
get parserIds() {
return this.topDownArray.map(particle => particle.definition.id)
}
get tags() {
return this.get("tags") || ""
}
get primaryTag() {
return this.tags.split(" ")[0]
}
get filenameNoExtension() {
return this.filename.replace(".scroll", "")
}
// todo: rename publishedUrl? Or something to indicate that this is only for stuff on the web (not localhost)
// BaseUrl must be provided for RSS Feeds and OpenGraph tags to work
get baseUrl() {
const baseUrl = (this.get("baseUrl") || "").replace(/\/$/, "")
return baseUrl + "/"
}
get canonicalUrl() {
return this.get("canonicalUrl") || this.baseUrl + this.permalink
}
get openGraphImage() {
const openGraphImage = this.get("openGraphImage")
if (openGraphImage !== undefined) return this.ensureAbsoluteLink(openGraphImage)
const images = this.filter(particle => particle.doesExtend("scrollImageParser"))
const hit = images.find(particle => particle.has("openGraph")) || images[0]
if (!hit) return ""
return this.ensureAbsoluteLink(hit.filename)
}
get absoluteLink() {
return this.ensureAbsoluteLink(this.permalink)
}
ensureAbsoluteLink(link) {
if (link.includes("://")) return link
return this.baseUrl + link.replace(/^\//, "")
}
get editUrl() {
const editUrl = this.get("editUrl")
if (editUrl) return editUrl
const editBaseUrl = this.get("editBaseUrl")
return (editBaseUrl ? editBaseUrl.replace(/\/$/, "") + "/" : "") + this.filename
}
get gitRepo() {
// given https://github.com/breck7/breckyunits.com/blob/main/four-tips-to-improve-communication.scroll
// return https://github.com/breck7/breckyunits.com
return this.editUrl.split("/").slice(0, 5).join("/")
}
get scrollVersion() {
// currently manually updated
return "161.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?)
// would speed up a lot.
get description() {
const description = this.getFromParserId("openGraphDescriptionParser")
if (description) return description
return this.generatedDescription
}
get generatedDescription() {
const firstParagraph = this.find(particle => particle.isArticleContent)
return firstParagraph ? firstParagraph.originalText.substr(0, 100).replace(/[&"<>']/g, "") : ""
}
get titleFromFilename() {
const unCamelCase = str => str.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, match => match.toUpperCase())
return unCamelCase(this.filenameNoExtension)
}
get title() {
return this.getFromParserId("scrollTitleParser") || this.titleFromFilename
}
get linkTitle() {
return this.getFromParserId("scrollLinkTitleParser") || this.title
}
get permalink() {
return this.get("permalink") || (this.filename ? this.filenameNoExtension + ".html" : "")
}
compileTo(extensionCapitalized) {
if (extensionCapitalized === "Txt")
return this.asTxt
if (extensionCapitalized === "Html")
return this.asHtml
const methodName = "build" + extensionCapitalized
return this.topDownArray
.filter(particle => particle[methodName])
.map((particle, index) => particle[methodName](index))
.join("\n")
.trim()
}
get asTxt() {
return (
this.map(particle => {
const text = particle.buildTxt ? particle.buildTxt() : ""
if (text) return text + "\n"
if (!particle.getLine().length) return "\n"
return ""
})
.join("")
.replace(/<[^>]*>/g, "")
.replace(/\n\n\n+/g, "\n\n") // Maximum 2 newlines in a row
.trim() + "\n" // Always end in a newline, Posix style
)
}
get dependencies() {
const dependencies = this.file.dependencies?.slice() || []
const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat()
return dependencies.concat(files)
}
get buildsHtml() {
const { permalink } = this
return !this.file.importOnly && (permalink.endsWith(".html") || permalink.endsWith(".htm"))
}
// Without specifying the language hyphenation will not work.
get lang() {
return this.get("htmlLang") || "en"
}
_compiledHtml = ""
get asHtml() {
if (!this._compiledHtml) {
const { permalink, buildsHtml } = this
const content = (this.buildHtml() + this.clearBodyStack()).trim()
// Don't add html tags to CSV feeds. A little hacky as calling a getter named _html_ to get _xml_ is not ideal. But
// <1% of use case so might be good enough.
const wrapWithHtmlTags = buildsHtml
const bodyTag = this.has("metaTags") ? "" : "\n"
this._compiledHtml = wrapWithHtmlTags ? `\n\n${bodyTag}${content}\n\n` : content
}
return this._compiledHtml
}
get wordCount() {
return this.asTxt.match(/\b\w+\b/g)?.length || 0
}
get minutes() {
return parseFloat((this.wordCount / 200).toFixed(1))
}
get date() {
const date = this.get("date") || (this.file.timestamp ? this.file.timestamp : 0)
return this.dayjs(date).format(`MM/DD/YYYY`)
}
get year() {
return parseInt(this.dayjs(this.date).format(`YYYY`))
}
get dayjs() {
if (!this.isNodeJs()) return dayjs
const lib = require("dayjs")
const relativeTime = require("dayjs/plugin/relativeTime")
lib.extend(relativeTime)
return lib
}
get lodash() {
return this.isNodeJs() ? require("lodash") : lodash
}
getConcepts(parsed) {
const concepts = []
let currentConcept
parsed.forEach(particle => {
if (particle.isConceptDelimiter) {
if (currentConcept) concepts.push(currentConcept)
currentConcept = []
}
if (currentConcept && particle.isMeasure) currentConcept.push(particle)
})
if (currentConcept) concepts.push(currentConcept)
return concepts
}
_formatConcepts(parsed) {
const concepts = this.getConcepts(parsed)
if (!concepts.length) return false
const {lodash} = this
// does a destructive sort in place on the parsed program
concepts.forEach(concept => {
let currentSection
const newCode = lodash
.sortBy(concept, ["sortIndex"])
.map(particle => {
let newLines = ""
const section = particle.sortIndex.toString().split(".")[0]
if (section !== currentSection) {
currentSection = section
newLines = "\n"
}
return newLines + particle.toString()
})
.join("\n")
concept.forEach((particle, index) => (index ? particle.destroy() : ""))
concept[0].replaceParticle(() => newCode)
})
}
get formatted() {
return this.getFormatted(this.file.codeAtStart)
}
get lastCommitTime() {
// todo: speed this up and do a proper release. also could add more metrics like this.
if (this._lastCommitTime === undefined) {
try {
this._lastCommitTime = require("child_process").execSync(`git log -1 --format="%at" -- "${this.filePath}"`).toString().trim()
} catch (err) {
this._lastCommitTime = 0
}
}
return this._lastCommitTime
}
getFormatted(codeAtStart = this.toString()) {
let formatted = codeAtStart.replace(/\r/g, "") // remove all carriage returns if there are any
const parsed = new this.constructor(formatted)
parsed.topDownArray.forEach(subparticle => {
subparticle.format()
const original = subparticle.getLine()
const trimmed = original.replace(/(\S.*?)[ \t]*$/gm, "$1")
// Trim trailing whitespace unless parser allows it
if (original !== trimmed && !subparticle.allowTrailingWhitespace) subparticle.setLine(trimmed)
})
this._formatConcepts(parsed)
let importOnlys = []
let topMatter = []
let allElse = []
// Create any bindings
parsed.forEach(particle => {
if (particle.bindTo === "next") particle.binding = particle.next
if (particle.bindTo === "previous") particle.binding = particle.previous
})
parsed.forEach(particle => {
if (particle.getLine() === "importOnly") importOnlys.push(particle)
else if (particle.isTopMatter) topMatter.push(particle)
else allElse.push(particle)
})
const combined = importOnlys.concat(topMatter, allElse)
// Move any bound particles
combined
.filter(particle => particle.bindTo)
.forEach(particle => {
// First remove the particle from its current position
const originalIndex = combined.indexOf(particle)
combined.splice(originalIndex, 1)
// Then insert it at the new position
// We need to find the binding index again after removal
const bindingIndex = combined.indexOf(particle.binding)
if (particle.bindTo === "next") combined.splice(bindingIndex, 0, particle)
else combined.splice(bindingIndex + 1, 0, particle)
})
const trimmed = combined
.map(particle => particle.toString())
.join("\n")
.replace(/^\n*/, "") // Remove leading newlines
.replace(/\n\n\n+/g, "\n\n") // Maximum 2 newlines in a row
.replace(/\n+$/, "")
return trimmed === "" ? trimmed : trimmed + "\n" // End non blank Scroll files in a newline character POSIX style for better working with tools like git
}
get parser() {
return this.constructor
}
get parsersRequiringExternals() {
const { parser } = this
// todo: could be cleaned up a bit
if (!parser.parsersRequiringExternals) parser.parsersRequiringExternals = parser.cachedHandParsersProgramRoot.filter(particle => particle.copyFromExternal).map(particle => particle.atoms[0])
return parser.parsersRequiringExternals
}
get Disk() { return this.isNodeJs() ? require("scrollsdk/products/Disk.node.js").Disk : {}}
async buildAll() {
await this.load()
await this.buildOne()
await this.buildTwo()
}
async buildOne() {
await this.execute()
const toBuild = this.filter(particle => particle.buildOne)
for (let particle of toBuild) {
await particle.buildOne()
}
}
async buildTwo(externalFilesCopied = {}) {
const toBuild = this.filter(particle => particle.buildTwo)
for (let particle of toBuild) {
await particle.buildTwo(externalFilesCopied)
}
}
_compileArray(filename, arr) {
const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== "")))
const parts = filename.split(".")
const format = parts.pop()
if (format === "json") return JSON.stringify(removeBlanks(arr), null, 2)
if (format === "js") return `const ${parts[0]} = ` + JSON.stringify(removeBlanks(arr), null, 2)
if (format === "csv") return this.arrayToCSV(arr)
if (format === "tsv") return this.arrayToCSV(arr, "\t")
if (format === "particles") return particles.toString()
return particles.toString()
}
makeLodashOrderByParams(str) {
const part1 = str.split(" ")
const part2 = part1.map(col => (col.startsWith("-") ? "desc" : "asc"))
return [part1.map(col => col.replace(/^\-/, "")), part2]
}
arrayToCSV(data, delimiter = ",") {
if (!data.length) return ""
// Extract headers
const headers = Object.keys(data[0])
const csv = data.map(row =>
headers
.map(fieldName => {
const fieldValue = row[fieldName]
// Escape commas if the value is a string
if (typeof fieldValue === "string" && fieldValue.includes(delimiter)) {
return `"${fieldValue.replace(/"/g, '""')}"` // Escape double quotes and wrap in double quotes
}
return fieldValue
})
.join(delimiter)
)
csv.unshift(headers.join(delimiter)) // Add header row at the top
return csv.join("\n")
}
compileConcepts(filename = "csv", sortBy = "") {
const {lodash} = this
if (!sortBy) return this._compileArray(filename, this.concepts)
const orderBy = this.makeLodashOrderByParams(sortBy)
return this._compileArray(filename, lodash.orderBy(this.concepts, orderBy[0], orderBy[1]))
}
_withStats
get measuresWithStats() {
if (!this._withStats) this._withStats = this.addMeasureStats(this.concepts, this.measures)
return this._withStats
}
addMeasureStats(concepts, measures){
return measures.map(measure => {
let Type = false
concepts.forEach(concept => {
const value = concept[measure.Name]
if (value === undefined || value === "") return
measure.Values++
if (!Type) {
measure.Example = value.toString().replace(/\n/g, " ")
measure.Type = typeof value
Type = true
}
})
measure.Coverage = Math.floor((100 * measure.Values) / concepts.length) + "%"
return measure
})
}
parseMeasures(parser) {
if (!Particle.measureCache)
Particle.measureCache = new Map()
const measureCache = Particle.measureCache
if (measureCache.get(parser)) return measureCache.get(parser)
const {lodash} = this
// todo: clean this up
const getCueAtoms = rootParserProgram =>
rootParserProgram
.filter(particle => particle.getLine().endsWith("Parser") && !particle.getLine().startsWith("abstract"))
.map(particle => particle.get("cue") || particle.getLine())
.map(line => line.replace(/Parser$/, ""))
// 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
const dummyProgram = new parser(
Array.from(
new Set(
getCueAtoms(parser.cachedHandParsersProgramRoot) // is there a better method name than this?
)
).join("\n")
)
// Delete any particles that are not measures
dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())
dummyProgram.forEach(particle => {
// add nested measures
Object.keys(particle.definition.cueMapWithDefinitions).forEach(key => particle.appendLine(key))
})
// Delete any nested particles that are not measures
dummyProgram.topDownArray.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy())
const measures = dummyProgram.topDownArray.map(particle => {
return {
Name: particle.measureName,
Values: 0,
Coverage: 0,
Question: particle.definition.description,
Example: particle.definition.getParticle("example")?.subparticlesToString() || "",
Type: particle.typeForWebForms,
Source: particle.sourceDomain,
//Definition: parsedProgram.root.filename + ":" + particle.lineNumber
SortIndex: particle.sortIndex,
IsComputed: particle.isComputed,
IsRequired: particle.isMeasureRequired,
IsConceptDelimiter: particle.isConceptDelimiter,
Cue: particle.definition.get("cue")
}
})
measureCache.set(parser, lodash.sortBy(measures, "SortIndex"))
return measureCache.get(parser)
}
_concepts
get concepts() {
if (this._concepts) return this._concepts
this._concepts = this.parseConcepts(this, this.measures)
return this._concepts
}
_measures
get measures() {
if (this._measures) return this._measures
this._measures = this.parseMeasures(this.parser)
return this._measures
}
parseConcepts(parsedProgram, measures){
// Todo: might be a perf/memory/simplicity win to have a "segment" method in ScrollSDK, where you could
// virtually split a Particle into multiple segments, and then query on those segments.
// So we would "segment" on "id ", and then not need to create a bunch of new objects, and the original
// already parsed lines could then learn about/access to their respective segments.
const conceptDelimiter = measures.filter(measure => measure.IsConceptDelimiter)[0]
if (!conceptDelimiter) return []
const concepts = parsedProgram.split(conceptDelimiter.Cue || conceptDelimiter.Name)
concepts.shift() // Remove the part before "id"
return concepts.map(concept => {
const row = {}
measures.forEach(measure => {
const measureName = measure.Name
const measureKey = measure.Cue || measureName.replace(/_/g, " ")
if (!measure.IsComputed) row[measureName] = concept.getParticle(measureKey)?.measureValue ?? ""
else row[measureName] = this.computeMeasure(parsedProgram, measureName, concept, concepts)
})
return row
})
}
computeMeasure(parsedProgram, measureName, concept, concepts){
// note that this is currently global, assuming there wont be. name conflicts in computed measures in a single scroll
if (!Particle.measureFnCache) Particle.measureFnCache = {}
const measureFnCache = Particle.measureFnCache
if (!measureFnCache[measureName]) {
// a bit hacky but works??
const particle = parsedProgram.appendLine(measureName)
measureFnCache[measureName] = particle.computeValue
particle.destroy()
}
return measureFnCache[measureName](concept, measureName, parsedProgram, concepts)
}
compileMeasures(filename = "csv", sortBy = "") {
const withStats = this.measuresWithStats
if (!sortBy) return this._compileArray(filename, withStats)
const orderBy = this.makeLodashOrderByParams(sortBy)
return this._compileArray(filename, this.lodash.orderBy(withStats, orderBy[0], orderBy[1]))
}
evalNodeJsMacros(value, macroMap, filePath) {
const tempPath = filePath + ".js"
const {Disk} = this
if (Disk.exists(tempPath)) throw new Error(`Failed to write/require replaceNodejs snippet since '${tempPath}' already exists.`)
try {
Disk.write(tempPath, value)
const results = require(tempPath)
Object.keys(results).forEach(key => (macroMap[key] = results[key]))
} catch (err) {
console.error(`Error in evalMacros in file '${filePath}'`)
console.error(err)
} finally {
Disk.rm(tempPath)
}
}
evalMacros(fusedFile) {
const {fusedCode, codeAtStart, filePath} = fusedFile
let code = fusedCode
const absolutePath = filePath
// note: the 2 params above are not used in this method, but may be used in user eval code. (todo: cleanup)
const regex = /^(replace|footer$)/gm
if (!regex.test(code)) return code
const particle = new Particle(code) // todo: this can be faster. a more lightweight particle class?
// Process macros
const macroMap = {}
particle
.filter(particle => {
const parserAtom = particle.cue
return parserAtom === "replace" || parserAtom === "replaceJs" || parserAtom === "replaceNodejs"
})
.forEach(particle => {
let value = particle.length ? particle.subparticlesToString() : particle.getAtomsFrom(2).join(" ")
const kind = particle.cue
if (kind === "replaceJs") value = eval(value)
if (this.isNodeJs() && kind === "replaceNodejs")
this.evalNodeJsMacros(value, macroMap, absolutePath)
else macroMap[particle.getAtom(1)] = value
particle.destroy() // Destroy definitions after eval
})
if (particle.has("footer")) {
const pushes = particle.getParticles("footer")
const append = pushes.map(push => push.section.join("\n")).join("\n")
pushes.forEach(push => {
push.section.forEach(particle => particle.destroy())
push.destroy()
})
code = particle.asString + append
}
const keys = Object.keys(macroMap)
if (!keys.length) return code
let codeAfterMacroSubstitution = particle.asString
// Todo: speed up. build a template?
Object.keys(macroMap).forEach(key => (codeAfterMacroSubstitution = codeAfterMacroSubstitution.replace(new RegExp(key, "g"), macroMap[key])))
return codeAfterMacroSubstitution
}
toRss() {
const { title, canonicalUrl } = this
return ` ${title}
${canonicalUrl}
${this.dayjs(this.timestamp * 1000).format("ddd, DD MMM YYYY HH:mm:ss ZZ")}`
}
static cachedHandParsersProgramRoot = new HandParsersProgram(`columnNameAtom
extends stringAtom
percentAtom
paint constant.numeric.float
extends stringAtom
// 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
countAtom
extends integerAtom
yearAtom
extends integerAtom
preBuildCommandAtom
extends cueAtom
description Give build command atoms their own color.
paint constant.character.escape
delimiterAtom
description String to use as a delimiter.
paint string
bulletPointAtom
description Any token used as a bullet point such as "-" or "1." or ">"
paint keyword
comparisonAtom
enum < > <= >= = != includes doesNotInclude empty notEmpty startsWith endsWith
paint constant
personNameAtom
extends stringAtom
urlAtom
paint constant.language
absoluteUrlAtom
paint constant.language
regex (ftp|https?)://.+
emailAddressAtom
extends stringAtom
permalinkAtom
paint string
description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.
filePathAtom
extends stringAtom
tagOrUrlAtom
description An HTML tag or a url.
paint constant.language
htmlAttributesAtom
paint comment
htmlTagAtom
paint constant.language
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
classNameAtom
paint constant
htmlIdAtom
extends anyAtom
fontFamilyAtom
enum Arial Helvetica Verdana Georgia Impact Tahoma Slim
paint constant
buildCommandAtom
extends cueAtom
description Give build command atoms their own color.
paint constant
cssAnyAtom
extends codeAtom
cssLengthAtom
extends codeAtom
htmlAnyAtom
extends codeAtom
inlineMarkupNameAtom
description Options to turn on some inline markups.
enum bold italics code katex none
scriptAnyAtom
extends codeAtom
tileOptionAtom
enum default light
measureNameAtom
extends cueAtom
// A regex for column names for max compatibility with a broad range of data science tools:
regex [a-zA-Z][a-zA-Z0-9]*
abstractConstantAtom
paint entity.name.tag
javascriptSafeAlphaNumericIdentifierAtom
regex [a-zA-Z0-9_]+
reservedAtoms enum extends function static if while export return class for default require var let const new
anyAtom
baseParsersAtom
description There are a few classes of special parsers. BlobParsers don't have their subparticles parsed. Error particles always report an error.
// todo Remove?
enum blobParser errorParser
paint variable.parameter
enumAtom
paint constant.language
booleanAtom
enum true false
extends enumAtom
atomParserAtom
enum prefix postfix omnifix
paint constant.numeric
atomPropertyNameAtom
paint variable.parameter
atomTypeIdAtom
examples integerAtom keywordAtom someCustomAtom
extends javascriptSafeAlphaNumericIdentifierAtom
enumFromAtomTypes atomTypeIdAtom
paint storage
constantIdentifierAtom
examples someId myVar
// todo Extend javascriptSafeAlphaNumericIdentifier
regex [a-zA-Z]\\w+
paint constant.other
description A atom that can be assigned to the parser in the target language.
constructorFilePathAtom
enumOptionAtom
// todo Add an enumOption top level type, so we can add data to an enum option such as a description.
paint string
atomExampleAtom
description Holds an example for a atom with a wide range of options.
paint string
extraAtom
paint invalid
fileExtensionAtom
examples js txt doc exe
regex [a-zA-Z0-9]+
paint string
numberAtom
paint constant.numeric
floatAtom
extends numberAtom
regex \\-?[0-9]*\\.?[0-9]*
paint constant.numeric.float
integerAtom
regex \\-?[0-9]+
extends numberAtom
paint constant.numeric.integer
cueAtom
description A atom that indicates a certain parser to use.
paint keyword
javascriptCodeAtom
lowercaseAtom
regex [a-z]+
parserIdAtom
examples commentParser addParser
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.
paint variable.parameter
extends javascriptSafeAlphaNumericIdentifierAtom
enumFromAtomTypes parserIdAtom
cueAtom
paint constant.language
regexAtom
paint string.regexp
reservedAtomAtom
description A atom that a atom cannot contain.
paint string
paintTypeAtom
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
paint string
scriptUrlAtom
semanticVersionAtom
examples 1.0.0 2.2.1
regex [0-9]+\\.[0-9]+\\.[0-9]+
paint constant.numeric
dateAtom
paint string
stringAtom
paint string
atomAtom
paint string
description A non-empty single atom string.
regex .+
exampleAnyAtom
examples lorem ipsem
// todo Eventually we want to be able to parse correctly the examples.
paint comment
extends stringAtom
blankAtom
commentAtom
paint comment
codeAtom
paint comment
javascriptAnyAtom
extends stringAtom
metaCommandAtom
extends cueAtom
description Give meta command atoms their own color.
paint constant.numeric
// Obviously this is not numeric. But I like the green color for now.
We need a better design to replace this "paint" concept
https://github.com/breck7/scrollsdk/issues/186
tagAtom
extends permalinkAtom
tagWithOptionalFolderAtom
description A group name optionally combined with a folder path. Only used when referencing tags, not in posts.
extends stringAtom
scrollThemeAtom
enum roboto gazette dark tufte prestige
paint constant
abstractScrollParser
atoms cueAtom
javascript
buildHtmlSnippet(buildSettings) {
return this.buildHtml(buildSettings)
}
buildTxt() {
return ""
}
getHtmlRequirements(buildSettings) {
const {requireOnce} = this
if (!requireOnce)
return ""
const set = buildSettings?.alreadyRequired || this.root.alreadyRequired
if (set.has(requireOnce))
return ""
set.add(requireOnce)
return requireOnce + "\\n\\n"
}
boolean suggestInAutocomplete false
abstractAftertextParser
description Text followed by markup commands.
extends abstractScrollParser
inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser aftertextTagParser abstractCommentParser
javascript
get markupInserts() {
const { originalTextPostLinkify } = this
return this.filter(particle => particle.isMarkup)
.map(particle => particle.getInserts(originalTextPostLinkify))
.filter(i => i)
.flat()
}
get originalText() {
return this.content ?? ""
}
get originalTextPostLinkify() {
const { originalText } = this
const shouldLinkify = this.get("linkify") === "false" || originalText.includes(" {
const needle = note.cue
const {linkBack} = note
if (originalText.includes(needle)) originalText = originalText.replace(new RegExp("\\\\" + needle + "\\\\b"), \`\${note.label}\`)
})
return originalText
}
get text() {
const { originalTextPostLinkify, markupInserts } = this
let adjustment = 0
let newText = originalTextPostLinkify
markupInserts.sort((a, b) => {
if (a.index !== b.index)
return a.index - b.index
// If multiple tags start at same index, the tag that closes first should start last. Otherwise HTML breaks.
if (b.index === b.endIndex) // unless the endIndex is the same as index
return a.endIndex - b.endIndex
return b.endIndex - a.endIndex
})
markupInserts.forEach(insertion => {
insertion.index += adjustment
const consumeStartCharacters = insertion.consumeStartCharacters ?? 0
const consumeEndCharacters = insertion.consumeEndCharacters ?? 0
newText = newText.slice(0, insertion.index - consumeEndCharacters) + insertion.string + newText.slice(insertion.index + consumeStartCharacters)
adjustment += insertion.string.length - consumeEndCharacters - consumeStartCharacters
})
return newText
}
tag = "p"
get className() {
if (this.get("classes"))
return this.get("classes")
const classLine = this.getParticle("class")
if (classLine && classLine.applyToParentElement) return classLine.content
return this.defaultClassName
}
defaultClassName = "scrollParagraph"
get isHidden() {
return this.has("hidden")
}
buildHtml(buildSettings) {
if (this.isHidden) return ""
this.buildSettings = buildSettings
const { className, styles } = this
const classAttr = className ? \`class="\${this.className}"\` : ""
const tag = this.get("tag") || this.tag
if (tag === "none") // Allow no tag for aftertext in tables
return this.text
const id = this.has("id") ? "" : \`id="\${this.htmlId}" \` // always add an html id
return this.getHtmlRequirements(buildSettings) + \`<\${tag} \${id}\${this.htmlAttributes}\${classAttr}\${styles}>\${this.text}\${this.closingTag}\`
}
get closingTag() {
const tag = this.get("tag") || this.tag
return \`\${tag}>\`
}
get htmlAttributes() {
const attrs = this.filter(particle => particle.isAttribute)
return attrs.length ? attrs.map(particle => particle.htmlAttributes).join(" ") + " " : ""
}
get styles() {
const style = this.getParticle("style")
const fontFamily = this.getParticle("font")
const color = this.getParticle("color")
if (!style && !fontFamily && !color)
return ""
return \` style="\${style?.content};\${fontFamily?.css};\${color?.css}"\`
}
get htmlId() {
return this.get("id") || "particle" + this.index
}
boolean suggestInAutocomplete false
scrollParagraphParser
// todo Perhaps rewrite this from scratch and move out of aftertext.
extends abstractAftertextParser
catchAllAtomType stringAtom
description A paragraph.
boolean suggestInAutocomplete false
cueFromId
javascript
buildHtml(buildSettings) {
if (this.isHidden) return ""
// Hacky, I know.
const newLine = this.has("inlineMarkupsOn") ? undefined : this.appendLine("inlineMarkupsOn")
const compiled = super.buildHtml(buildSettings)
if (newLine)
newLine.destroy()
return compiled
}
buildTxt() {
const subparticles = this.filter(particle => particle.buildTxt).map(particle => particle.buildTxt()).filter(i => i).join("\\n")
const dateline = this.getParticle("dateline")
return (dateline ? dateline.day + "\\n\\n" : "") + (this.originalText || "") + (subparticles ? "\\n " + subparticles.replace(/\\n/g, "\\n ") : "")
}
boolean suggestInAutocomplete false
authorsParser
popularity 0.007379
// multiple authors delimited by " and "
boolean isPopular true
extends scrollParagraphParser
description Set author(s) name(s).
example
authors Breck Yunits
https://breckyunits.com Breck Yunits
// note: once we have mixins in Parsers, lets mixin the below from abstractTopLevelSingleMetaParser
atoms metaCommandAtom
javascript
isTopMatter = true
isSetterParser = true
buildHtmlForPrint() {
// hacky. todo: cleanup
const originalContent = this.content
this.setContent(\`by \${originalContent}\`)
const html = super.buildHtml()
this.setContent(originalContent)
return html
}
buildTxtForPrint() {
return 'by ' + super.buildTxt()
}
buildHtml() {
return ""
}
buildTxt() {
return ""
}
defaultClassName = "printAuthorsParser"
boolean suggestInAutocomplete false
blinkParser
description Just for fun.
extends scrollParagraphParser
example
blink Carpe diem!
cue blink
javascript
buildHtml() {
return \`\${super.buildHtml()}
\`
}
boolean suggestInAutocomplete false
scrollButtonParser
extends scrollParagraphParser
cue button
description A button.
postParser
description Post a particle.
example
button Click me
javascript
defaultClassName = "scrollButton"
tag = "button"
get htmlAttributes() {
const link = this.getFromParser("linkParser")
const post = this.getParticle("post")
if (post) {
const method = "post"
const action = link?.link || ""
const formData = new URLSearchParams({particle: post.subparticlesToString()}).toString()
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;" \`
}
return super.htmlAttributes + (link ? \`onclick="window.location='\${link.link}'"\` : "")
}
getFromParser(parserId) {
return this.find(particle => particle.doesExtend(parserId))
}
boolean suggestInAutocomplete false
catchAllParagraphParser
popularity 0.115562
description A paragraph.
extends scrollParagraphParser
boolean suggestInAutocomplete false
boolean isPopular true
boolean isArticleContent true
atoms stringAtom
javascript
getErrors() {
const errors = super.getErrors() || []
return this.parent.has("testStrict") ? errors.concat(this.makeError(\`catchAllParagraphParser should not have any matches when testing with testStrict.\`)) : errors
}
get originalText() {
return this.getLine() || ""
}
boolean suggestInAutocomplete false
scrollCenterParser
popularity 0.006415
cue center
description A centered section.
extends scrollParagraphParser
example
center
This paragraph is centered.
javascript
buildHtml() {
this.parent.sectionStack.push("
")
return \`
\${super.buildHtml()}\`
}
buildTxt() {
return this.content
}
boolean suggestInAutocomplete false
abstractIndentableParagraphParser
extends scrollParagraphParser
inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser
javascript
compileSubparticles() {
return this.map(particle => particle.buildHtml())
.join("\\n")
.trim()
}
buildHtml() {
return super.buildHtml() + this.compileSubparticles()
}
buildTxt() {
return this.getAtom(0) + " " + super.buildTxt()
}
boolean suggestInAutocomplete false
checklistTodoParser
popularity 0.000193
extends abstractIndentableParagraphParser
example
[] Get milk
description A task todo.
cue []
string checked
javascript
get text() {
return \`\`
}
get id() {
return this.get("id") || "item" + this._getUid()
}
boolean suggestInAutocomplete false
checklistDoneParser
popularity 0.000072
extends checklistTodoParser
description A completed task.
string checked checked
cue [x]
example
[x] get milk
boolean suggestInAutocomplete false
listAftertextParser
popularity 0.014325
extends abstractIndentableParagraphParser
example
- I had a _new_ thought.
description A list item.
cue -
javascript
defaultClassName = ""
buildHtml() {
const {index, parent} = this
const particleClass = this.constructor
const isStartOfList = index === 0 || !(parent.particleAt(index - 1) instanceof particleClass)
const isEndOfList = parent.length === index + 1 || !(parent.particleAt(index + 1) instanceof particleClass)
const { listType } = this
return (isStartOfList ? \`<\${listType} \${this.attributes}>\` : "") + \`\${super.buildHtml()}\` + (isEndOfList ? \`\${listType}>\` : "")
}
get attributes() {
return ""
}
tag = "li"
listType = "ul"
boolean suggestInAutocomplete false
abstractCustomListItemParser
extends listAftertextParser
javascript
get requireOnce() {
return \`\`
}
get attributes() {
return \`class="\${this.constructor.name}"\`
}
boolean suggestInAutocomplete false
orderedListAftertextParser
popularity 0.004485
extends listAftertextParser
description A list item.
example
1. Hello world
pattern ^\\d+\\.
javascript
listType = "ol"
get attributes() { return \` start="\${this.getAtom(0)}"\`}
boolean suggestInAutocomplete false
quickQuoteParser
popularity 0.000482
cue >
example
> The only thing we have to fear is fear itself. - FDR
boolean isPopular true
extends abstractIndentableParagraphParser
description A quote.
javascript
defaultClassName = "scrollQuote"
tag = "blockquote"
boolean suggestInAutocomplete false
scrollCounterParser
description Visualize the speed of something.
extends scrollParagraphParser
cue counter
example
counter 4.5 Babies Born
atoms cueAtom numberAtom
javascript
buildHtml() {
const line = this.getLine()
const atoms = line.split(" ")
atoms.shift() // drop the counter atom
const perSecond = parseFloat(atoms.shift()) // get number
const increment = perSecond/10
const id = this._getUid()
this.setLine(\`* 0 \` + atoms.join(" "))
const html = super.buildHtml()
this.setLine(line)
return html
}
boolean suggestInAutocomplete false
expanderParser
popularity 0.000072
cueFromId
description An collapsible HTML details tag.
extends scrollParagraphParser
example
expander Knock Knock
Who's there?
javascript
buildHtml() {
this.parent.sectionStack.push("")
return \`\${super.buildHtml()}\`
}
buildTxt() {
return this.content
}
tag = "summary"
defaultClassName = ""
boolean suggestInAutocomplete false
footnoteDefinitionParser
popularity 0.001953
description A footnote. Can also be used as section notes.
extends scrollParagraphParser
boolean isFootnote true
pattern ^\\^.+$
// 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.
inScope quickLinkParser
labelParser
description If you want to show a custom label for a footnote. Default label is the note definition index.
cueFromId
atoms cueAtom
catchAllAtomType stringAtom
javascript
get htmlId() {
return \`note\${this.noteDefinitionIndex}\`
}
get label() {
// In the future we could allow common practices like author name
return this.get("label") || \`[\${this.noteDefinitionIndex}]\`
}
get linkBack() {
return \`noteUsage\${this.noteDefinitionIndex}\`
}
get text() {
return \`\${this.label} \${super.text}\`
}
get noteDefinitionIndex() {
return this.parent.footnotes.indexOf(this) + 1
}
buildTxt() {
return this.getAtom(0) + ": " + super.buildTxt()
}
boolean suggestInAutocomplete false
abstractHeaderParser
extends scrollParagraphParser
example
# Hello world
javascript
buildHtml(buildSettings) {
if (this.isHidden) return ""
if (this.parent.sectionStack)
this.parent.sectionStack.push("")
return \`
\` + super.buildHtml(buildSettings)
}
buildTxt() {
const line = super.buildTxt()
return line + "\\n" + "=".repeat(line.length)
}
isHeader = true
boolean suggestInAutocomplete false
h1Parser
popularity 0.017918
description An html h1 tag.
extends abstractHeaderParser
boolean isArticleContent true
cue #
boolean isPopular true
javascript
tag = "h1"
boolean suggestInAutocomplete false
h2Parser
popularity 0.005257
description An html h2 tag.
extends abstractHeaderParser
boolean isArticleContent true
cue ##
boolean isPopular true
javascript
tag = "h2"
boolean suggestInAutocomplete false
h3Parser
popularity 0.001085
description An html h3 tag.
extends abstractHeaderParser
boolean isArticleContent true
cue ###
javascript
tag = "h3"
boolean suggestInAutocomplete false
h4Parser
popularity 0.000289
description An html h4 tag.
extends abstractHeaderParser
cue ####
javascript
tag = "h4"
boolean suggestInAutocomplete false
scrollQuestionParser
popularity 0.004244
description A question.
extends h4Parser
cue ?
example
? Why is the sky blue?
javascript
defaultClassName = "scrollQuestion"
boolean suggestInAutocomplete false
h5Parser
description An html h5 tag.
extends abstractHeaderParser
cue #####
javascript
tag = "h5"
boolean suggestInAutocomplete false
printTitleParser
popularity 0.007572
description Print title.
extends abstractHeaderParser
boolean isPopular true
example
title Eureka
printTitle
cueFromId
javascript
buildHtml(buildSettings) {
// Hacky, I know.
const {content} = this
if (content === undefined)
this.setContent(this.root.title)
const { permalink } = this.root
if (!permalink) {
this.setContent(content) // Restore it as it was.
return super.buildHtml(buildSettings)
}
const newLine = this.appendLine(\`link \${permalink}\`)
const compiled = super.buildHtml(buildSettings)
newLine.destroy()
this.setContent(content) // Restore it as it was.
return compiled
}
get originalText() {
return this.content ?? this.root.title ?? ""
}
defaultClassName = "printTitleParser"
tag = "h1"
boolean suggestInAutocomplete false
captionAftertextParser
popularity 0.003207
description An image caption.
cue caption
extends scrollParagraphParser
boolean isPopular true
boolean suggestInAutocomplete false
abstractMediaParser
extends scrollParagraphParser
inScope scrollMediaLoopParser scrollAutoplayParser
int atomIndex 1
javascript
buildTxt() {
return ""
}
get filename() {
return this.getAtom(this.atomIndex)
}
getAsHtmlAttribute(attr) {
if (!this.has(attr)) return ""
const value = this.get(attr)
return value ? \`\${attr}="\${value}"\` : attr
}
getAsHtmlAttributes(list) {
return list.map(atom => this.getAsHtmlAttribute(atom)).filter(i => i).join(" ")
}
buildHtml() {
return \`<\${this.tag} src="\${this.filename}" controls \${this.getAsHtmlAttributes("width height loop autoplay".split(" "))}>\${this.tag}>\`
}
boolean suggestInAutocomplete false
scrollMusicParser
popularity 0.000024
extends abstractMediaParser
cue music
description Play sound files.
example
music sipOfCoffee.m4a
javascript
buildHtml() {
return \`\`
}
boolean suggestInAutocomplete false
quickSoundParser
popularity 0.000024
extends scrollMusicParser
atoms urlAtom
pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)
int atomIndex 0
boolean suggestInAutocomplete false
scrollVideoParser
popularity 0.000024
extends abstractMediaParser
cue video
example
video spirit.mp4
description Play video files.
widthParser
cueFromId
atoms cueAtom
heightParser
cueFromId
atoms cueAtom
javascript
tag = "video"
boolean suggestInAutocomplete false
quickVideoParser
popularity 0.000024
extends scrollVideoParser
atoms urlAtom
pattern ^[^\\s]+\\.(mp4|webm|avi|mov)
int atomIndex 0
widthParser
// todo: fix inheritance bug
cueFromId
atoms cueAtom
heightParser
cueFromId
atoms cueAtom
boolean suggestInAutocomplete false
quickParagraphParser
popularity 0.001881
cue *
extends scrollParagraphParser
description A paragraph.
boolean isArticleContent true
example
* I had a _new_ idea.
boolean suggestInAutocomplete false
scrollStopwatchParser
description A stopwatch.
extends scrollParagraphParser
cue stopwatch
example
stopwatch
atoms cueAtom
catchAllAtomType numberAtom
javascript
buildHtml() {
const line = this.getLine()
const id = this._getUid()
this.setLine(\`* 0.0 \`)
const html = super.buildHtml()
this.setLine(line)
return html
}
boolean suggestInAutocomplete false
thinColumnsParser
popularity 0.003690
extends abstractAftertextParser
cueFromId
catchAllAtomType integerAtom
description Thin columns.
javascript
buildHtmlSnippet() {
return ""
}
columnWidth = 35
columnGap = 20
buildHtml() {
const {columnWidth, columnGap, maxColumns} = this
const maxTotalWidth = maxColumns * columnWidth + (maxColumns - 1) * columnGap
const stackContents = this.parent.clearSectionStack() // Starting columns always first clears the section stack.
if (this.singleColumn) this.parent.sectionStack.push("
") // Single columns are self-closing after section break.
return stackContents + \`