`
}
}
class editLinkParser extends abstractTextLinkParser {
get text() { return `Edit` }
get link() {
return this.root.editUrl || ""
}
}
class scrollVersionLinkParser extends abstractTextLinkParser {
get link() { return `https://scroll.pub` }
get text() {
return `Built with Scroll v${this.root.scrollVersion}`
}
}
class classicFormParser extends abstractAftertextParser {
get cueAtom() {
return this.getAtom(0)
}
get stringAtom() {
return this.getAtomsFrom(1)
}
get style() { return `` }
get script() { return `` }
get inputs() {
return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {
const {Name, Question, IsRequired, Type} = measure
const type = Type || "text"
const placeholder = Question
const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)
// ${index ? "" : "autofocus"}
let tag = ""
if (Type === "textarea")
tag = ``
else
tag = ``
return `
${tag}
`
}).join("\n")
}
buildHtml() {
const {isEmail, formDestination, callToAction, subject} = this
return `${this.script}${this.style}`
}
get callToAction() {
return (this.isEmail ? "Submit via email" : (this.subject || "Post"))
}
get isEmail() {
return this.formDestination.includes("@")
}
get formDestination() {
return this.getAtom(1) || ""
}
get subject() {
return this.getAtomsFrom(2)?.join(" ") || ""
}
get footer() {
return ""
}
}
class scrollFormParser extends classicFormParser {
createParserPool() {class placeholderParser extends ParserBackedParticle {
createParserPool() { return new Particle.ParserPool(this._getBlobParserCatchAllParser())}
getErrors() { return [] }
get cueAtom() {
return this.getAtom(0)
}
}
class valueParser extends ParserBackedParticle {
createParserPool() { return new Particle.ParserPool(this._getBlobParserCatchAllParser())}
getErrors() { return [] }
get cueAtom() {
return this.getAtom(0)
}
}
class nameParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get stringAtom() {
return this.getAtom(1)
}
}
return new Particle.ParserPool(undefined, Object.assign(Object.assign({}, super.createParserPool()._getCueMapAsObject()), {"placeholder" : placeholderParser,
"value" : valueParser,
"name" : nameParser}), undefined)
}
get requireOnce() { return `
` }
get copyFromExternal() { return `.codeMirror.css .scrollLibs.js .constants.js` }
get placeholder() {
return this.getParticle("placeholder")?.subparticlesToString() || ""
}
get value() {
return this.getParticle("value")?.subparticlesToString() || ""
}
get footer() {
return ""
}
get name() {
return this.get("name") || "particles"
}
get parsersBundle() {
const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm
const clone = this.root.clone()
const parsers = clone.filter(line => parserRegex.test(line.getLine()))
return "\n" + parsers.map(particle => {
particle.prependLine("boolean suggestInAutocomplete true")
return particle.toString()
}).join("\n")
}
get inputs() {
const Name = this.name
return `
`
}
buildHtml(buildSettings) {
return this.getHtmlRequirements(buildSettings) + super.buildHtml()
}
}
class loremIpsumParser extends abstractAftertextParser {
get integerAtom() {
return this.getAtomsFrom(0).map(val => parseInt(val))
}
get placeholder() { return `Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.` }
get originalText() {
return this.placeholder.repeat(this.howMany)
}
get howMany() {
return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1
}
}
class nickelbackIpsumParser extends loremIpsumParser {
get placeholder() { return `And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?` }
}
class scrollModalParser extends abstractAftertextParser {
get requireOnce() { return `
` }
get isHtml() { return true }
buildHtml(buildSettings) {
this.parent.sectionStack.push("")
return this.getHtmlRequirements(buildSettings) + `
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 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)
}
}
class abstractEmailMeasureParser extends abstractAtomMeasureParser {
get measureNameAtom() {
return this.getAtom(0)
}
get emailAddressAtom() {
return this.getAtom(1)
}
get typeForWebForms() { return `email` }
}
class abstractUrlMeasureParser extends abstractAtomMeasureParser {
get measureNameAtom() {
return this.getAtom(0)
}
get urlAtom() {
return this.getAtom(1)
}
get typeForWebForms() { return `url` }
}
class abstractStringMeasureParser extends abstractMeasureParser {
get stringAtom() {
return this.getAtomsFrom(0)
}
}
class abstractIdParser extends abstractStringMeasureParser {
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 abstractIdMeasureParser extends abstractIdParser {
}
class abstractTextareaMeasureParser extends abstractMeasureParser {
createParserPool() { return new Particle.ParserPool(this._getBlobParserCatchAllParser())}
getErrors() { return [] }
get typeForWebForms() { return `textarea` }
get measureValue() {
return this.subparticlesToString().replace(/\n/g, "\\n")
}
}
class abstractNumericMeasureParser extends abstractMeasureParser {
get typeForWebForms() { return `number` }
get measureValue() {
const {content} = this
return content === undefined ? "" : parseFloat(content)
}
}
class abstractNumberMeasureParser extends abstractNumericMeasureParser {
}
class abstractIntegerMeasureParser extends abstractNumericMeasureParser {
get measureNameAtom() {
return this.getAtom(0)
}
get integerAtom() {
return parseInt(this.getAtom(1))
}
get measureValue() {
const {content} = this
return content === undefined ? "" : parseInt(content)
}
}
class abstractIntMeasureParser extends abstractIntegerMeasureParser {
}
class abstractFloatMeasureParser extends abstractNumericMeasureParser {
get measureNameAtom() {
return this.getAtom(0)
}
get floatAtom() {
return parseFloat(this.getAtom(1))
}
}
class abstractPercentageMeasureParser extends abstractNumericMeasureParser {
get measureNameAtom() {
return this.getAtom(0)
}
get percentAtom() {
return this.getAtom(1)
}
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)
}
}
class abstractBooleanMeasureParser extends abstractMeasureParser {
get measureNameAtom() {
return this.getAtom(0)
}
get booleanAtom() {
return this.getAtom(1)
}
get measureValue() {
const {content} = this
return content === undefined ? "" : content == "true"
}
}
class abstractDateMeasureParser extends abstractMeasureParser {
get measureNameAtom() {
return this.getAtom(0)
}
get dateAtom() {
return this.getAtom(1)
}
get typeForWebForms() { return `date` }
get measureValue() {
const {content} = this
if (!content) return ""
const {dayjs} = this.root
try {
// First try parsing with dayjs
const parsed = dayjs(content)
if (parsed.isValid())
return parsed.format("YYYY-MM-DD")
// Try parsing other common formats
const formats = [
"MM/DD/YYYY",
"DD/MM/YYYY",
"YYYY/MM/DD",
"MM-DD-YYYY",
"DD-MM-YYYY",
"YYYY-MM-DD",
"DD.MM.YYYY",
"YYYY.MM.DD"
]
for (const format of formats) {
const attempt = dayjs(content, format)
if (attempt.isValid())
return attempt.format("YYYY-MM-DD")
}
} catch (err) {
console.error(err)
return ""
}
return ""
}
get valueAsTimestamp() {
const {measureValue} = this
return measureValue ? this.root.dayjs(measureValue).unix() : ""
}
}
class metaTagsParser extends abstractScrollParser {
buildHtmlSnippet() {
return ""
}
buildHtml() {
const {root} = this
const { title, description, canonicalUrl, gitRepo, scrollVersion, openGraphImage, keywords, filename } = root
const rssFeedUrl = root.get("rssFeedUrl")
const favicon = root.get("favicon")
const faviconTag = favicon ? `` : ""
const keywordsTag = keywords ? `` : ""
const rssTag = rssFeedUrl ? `` : ""
const gitTag = gitRepo ? `` : ""
return `
${title}
${keywordsTag}
${faviconTag}
${gitTag}
${rssTag}
`
}
}
class quoteParser extends abstractScrollParser {
createParserPool() {
return new Particle.ParserPool(quoteLineParser, undefined, undefined)
}
buildHtml() {
return `
${this.subparticlesToString()}
`
}
buildTxt() {
return this.subparticlesToString()
}
}
class redirectToParser extends abstractScrollParser {
get cueAtom() {
return this.getAtom(0)
}
get urlAtom() {
return this.getAtom(1)
}
buildHtml() {
return ``
}
}
class abstractVariableParser extends abstractScrollParser {
get preBuildCommandAtom() {
return this.getAtom(0)
}
get stringAtom() {
return this.getAtomsFrom(1)
}
isTopMatter = true
buildHtml() {
return ""
}
}
class replaceParser extends abstractVariableParser {
createParserPool() { return new Particle.ParserPool(this._getBlobParserCatchAllParser())}
getErrors() { return [] }
}
class replaceJsParser extends replaceParser {
createParserPool() { return new Particle.ParserPool(this._getBlobParserCatchAllParser())}
getErrors() { return [] }
get javascriptAnyAtom() {
return this.getAtomsFrom(0)
}
}
class replaceNodejsParser extends abstractVariableParser {
createParserPool() { return new Particle.ParserPool(this._getBlobParserCatchAllParser())}
getErrors() { return [] }
get javascriptAnyAtom() {
return this.getAtomsFrom(0)
}
}
class toFooterParser extends abstractScrollParser {
get preBuildCommandAtom() {
return this.getAtom(0)
}
}
class runScriptParser extends abstractScrollParser {
get cueAtom() {
return this.getAtom(0)
}
get urlAtom() {
return this.getAtom(1)
}
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 filenameIndex() { return 0 }
}
class endSnippetParser extends abstractScrollParser {
buildHtml() {
return ""
}
}
class toStampParser extends abstractScrollParser {
get filePathAtom() {
return this.getAtomsFrom(0)
}
buildTxt() {
return this.makeStamp(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 gitOnly = this.has("gitOnly");
const excludePatterns = this.get("exclude")?.split(" ") || [".git/**", "node_modules/**"];
const maxDepth = parseInt(this.get("maxDepth")) || Infinity;
const gitTrackedFiles = new Set(
gitOnly ? execSync('git ls-files', { cwd: dir, encoding: 'utf-8' })
.split('\n')
.filter(Boolean)
: []
);
// Convert glob patterns to RegExp
const excludeRegexes = excludePatterns.map(pattern =>
new RegExp('^' + pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*') + '$')
);
const shouldInclude = (relativePath, isDirectory) => {
if (excludeRegexes.some(regex => regex.test(relativePath)))
return false;
if (isDirectory)
return true
if (gitOnly && !gitTrackedFiles.has(relativePath))
return false;
return true;
};
const handleFile = (indentation, relativePath, itemPath) => {
if (!shouldInclude(relativePath)) return;
stamp += `${indentation}${relativePath}\n`;
try {
const content = fs.readFileSync(itemPath, { encoding: 'utf8', flag: 'r' });
// Skip if file appears to be binary
if (content.includes('\0') || /[\x00-\x08\x0E-\x1F]/.test(content)) return;
stamp += `${indentation} ${content.replace(/\n/g, `\n${indentation} `)}\n`;
} catch (err) {
// Skip files that can't be read as utf8
console.error(`Error reading ${itemPath}: ${err.message}`);
return;
}
};
function processDirectory(currentPath, depth = 0) {
if (depth > maxDepth) return;
const items = fs.readdirSync(currentPath);
items.forEach(item => {
const itemPath = path.join(currentPath, item);
const relativePath = path.relative(dir, itemPath);
const stats = fs.statSync(itemPath);
const isDirectory = stats.isDirectory()
if (!shouldInclude(relativePath, isDirectory)) return;
const indentation = ' '.repeat(depth);
if (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()) {
processDirectory(dir, 1);
} else {
handleFile(" ", dir, dir);
}
return stamp.trim();
}
}
class stampParser extends abstractScrollParser {
createParserPool() {
return new Particle.ParserPool(stampFileParser, undefined, [{regex: /\/$/, parser: stampFolderParser}])
}
get preBuildCommandAtom() {
return this.getAtom(0)
}
execute() {
const dir = this.root.folderPath
this.forEach(particle => particle.execute(dir))
}
}
class scrollStumpParser extends abstractScrollParser {
createParserPool() {
return new Particle.ParserPool(stumpContentParser, undefined, undefined)
}
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 {
buildHtmlSnippet() {
return ""
}
}
class plainTextParser extends abstractScrollParser {
createParserPool() {
return new Particle.ParserPool(plainTextLineParser, undefined, undefined)
}
get stringAtom() {
return this.getAtomsFrom(0)
}
buildHtml() {
return this.buildTxt()
}
buildTxt() {
return `${this.content ?? ""}${this.subparticlesToString()}`
}
}
class plainTextOnlyParser extends plainTextParser {
buildHtml() {
return ""
}
}
class scrollThemeParser extends abstractScrollParser {
get scrollThemeAtom() {
return this.getAtomsFrom(0)
}
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`).concat([".scroll.css"])
}
buildHtml() {
return this.files.map(name => ``).join("\n")
}
}
class abstractAftertextAttributeParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
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)
}
}
class aftertextStyleParser extends abstractAftertextAttributeParser {
get cssAnyAtom() {
return this.getAtomsFrom(0)
}
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 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 property() { return `color` }
}
class aftertextHrefParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get urlAtom() {
return this.getAtom(1)
}
}
class aftertextSrcParser extends aftertextHrefParser {
}
class aftertextOnclickParser extends abstractAftertextAttributeParser {
get javascriptAnyAtom() {
return this.getAtomsFrom(0)
}
}
class aftertextHiddenParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
}
class aftertextValueParser extends abstractAftertextAttributeParser {
get htmlAnyAtom() {
return this.getAtomsFrom(0)
}
}
class aftertextForParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get htmlAnyAtom() {
return this.getAtom(1)
}
}
class aftertextPlaceholderParser extends abstractAftertextAttributeParser {
get htmlAnyAtom() {
return this.getAtomsFrom(0)
}
}
class aftertextRowsParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get integerAtom() {
return parseInt(this.getAtom(1))
}
}
class aftertextTypeParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get htmlTypeAtom() {
return this.getAtom(1)
}
}
class aftertextAltParser extends abstractAftertextAttributeParser {
get htmlAnyAtom() {
return this.getAtomsFrom(0)
}
}
class aftertextTitleParser extends abstractAftertextAttributeParser {
get htmlAnyAtom() {
return this.getAtomsFrom(0)
}
}
class aftertextNameParser extends abstractAftertextAttributeParser {
get htmlAnyAtom() {
return this.getAtomsFrom(0)
}
}
class aftertextOnsubmitParser extends abstractAftertextAttributeParser {
get javascriptAnyAtom() {
return this.getAtomsFrom(0)
}
}
class aftertextClassParser extends abstractAftertextAttributeParser {
get classNameAtom() {
return this.getAtomsFrom(0)
}
}
class aftertextMaxlengthParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get integerAtom() {
return parseInt(this.getAtom(1))
}
}
class aftertextPatternParser extends abstractAftertextAttributeParser {
get htmlAnyAtom() {
return this.getAtomsFrom(0)
}
}
class aftertextRequiredParser extends abstractAftertextAttributeParser {
}
class aftertextDisabledParser extends abstractAftertextAttributeParser {
}
class aftertextReadonlyParser extends abstractAftertextAttributeParser {
}
class aftertextAriaLabelParser extends abstractAftertextAttributeParser {
get htmlAnyAtom() {
return this.getAtomsFrom(0)
}
}
class aftertextTargetParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get htmlAnyAtom() {
return this.getAtom(1)
}
}
class aftertextContentParser extends abstractAftertextAttributeParser {
get htmlAnyAtom() {
return this.getAtomsFrom(0)
}
}
class aftertextMinParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get numberAtom() {
return parseFloat(this.getAtom(1))
}
}
class aftertextMaxParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get numberAtom() {
return parseFloat(this.getAtom(1))
}
}
class aftertextStepParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get numberAtom() {
return parseFloat(this.getAtom(1))
}
}
class aftertextWidthParser extends abstractAftertextAttributeParser {
get cssAnyAtom() {
return this.getAtomsFrom(0)
}
}
class aftertextHeightParser extends abstractAftertextAttributeParser {
get cssAnyAtom() {
return this.getAtomsFrom(0)
}
}
class aftertextDataParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get htmlAnyAtom() {
return this.getAtomsFrom(1)
}
}
class aftertextRoleParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get htmlAnyAtom() {
return this.getAtom(1)
}
}
class aftertextTabindexParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get integerAtom() {
return parseInt(this.getAtom(1))
}
}
class aftertextColspanParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get integerAtom() {
return parseInt(this.getAtom(1))
}
}
class aftertextRowspanParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get integerAtom() {
return parseInt(this.getAtom(1))
}
}
class aftertextMethodParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get htmlMethodAtom() {
return this.getAtom(1)
}
}
class aftertextActionParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get urlAtom() {
return this.getAtom(1)
}
}
class aftertextAutocompleteParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get htmlAnyAtom() {
return this.getAtom(1)
}
}
class aftertextMultipleParser extends abstractAftertextAttributeParser {
}
class aftertextSelectedParser extends abstractAftertextAttributeParser {
}
class aftertextCheckedParser extends abstractAftertextAttributeParser {
}
class aftertextRelParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get htmlAnyAtom() {
return this.getAtom(1)
}
}
class aftertextEnctypeParser extends abstractAftertextAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get htmlAnyAtom() {
return this.getAtom(1)
}
}
class aftertextAcceptParser extends abstractAftertextAttributeParser {
get mimeTypeAtom() {
return this.getAtomsFrom(0)
}
}
class aftertextTagParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get htmlTagAtom() {
return this.getAtom(1)
}
buildHtml() {
return ""
}
}
class abstractAftertextDirectiveParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get stringAtom() {
return this.getAtomsFrom(1)
}
isMarkup = true
buildHtml() {
return ""
}
getErrors() {
if (this.silenceErrors) return [] // todo: generalize this?
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 {
createParserPool() {
return new Particle.ParserPool(undefined, Object.assign(Object.assign({}, super.createParserPool()._getCueMapAsObject()), {"matchAll" : matchAllParser,
"match" : matchParser}), undefined)
}
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 {
tag = "b"
}
class italicsParser extends abstractMarkupParser {
tag = "i"
}
class underlineParser extends abstractMarkupParser {
tag = "u"
}
class afterTextCenterParser extends abstractMarkupParser {
tag = "center"
}
class aftertextCodeParser extends abstractMarkupParser {
tag = "code"
}
class aftertextStrikeParser extends abstractMarkupParser {
tag = "s"
}
class addClassMarkupParser extends abstractMarkupParser {
get cueAtom() {
return this.getAtom(0)
}
get classNameAtom() {
return this.getAtom(1)
}
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 hoverNoteParser extends addClassMarkupParser {
createParserPool() {
return new Particle.ParserPool(lineOfTextParser, undefined, undefined)
}
get cueAtom() {
return this.getAtom(0)
}
get pattern() {
return this.getAtomsFrom(1).join(" ")
}
get attributes() {
return [`class="scrollHoverNote"`, `title="${this.hoverNoteText}"`]
}
get hoverNoteText() {
return this.subparticlesToString().replace(/\n/g, " ")
}
}
class scrollLinkParser extends abstractMarkupParser {
createParserPool() {class programParser extends ParserBackedParticle {
createParserPool() {
return new Particle.ParserPool(programLinkParser, undefined, undefined)
}
get cueAtom() {
return this.getAtom(0)
}
get encoded() {
return encodeURIComponent(this.subparticlesToString())
}
}
return new Particle.ParserPool(undefined, Object.assign(Object.assign({}, super.createParserPool()._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)
}
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 linkAttribute() {
return "href"
}
get attributes() {
const attrs = [`${this.linkAttribute}="${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 scrollClickParser extends scrollLinkParser {
get linkAttribute() {
return "onclick"
}
}
class emailLinkParser extends scrollLinkParser {
get attributes() {
return [`href="mailto:${this.link}"`]
}
}
class quickLinkParser extends scrollLinkParser {
get urlAtom() {
return this.getAtom(0)
}
get link() {
return this.cue
}
patternStartsAtAtom = 1
}
class quickRelativeLinkParser extends scrollLinkParser {
get urlAtom() {
return this.getAtom(0)
}
get link() {
return this.cue
}
patternStartsAtAtom = 1
}
class datelineParser extends abstractAftertextDirectiveParser {
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 {
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 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)
}
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)
}
}
class abstractMarkupParameterParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
}
class matchAllParser extends abstractMarkupParameterParser {
}
class matchParser extends abstractMarkupParameterParser {
get integerAtom() {
return this.getAtomsFrom(0).map(val => parseInt(val))
}
get indexes() {
return this.getAtomsFrom(1).map(num => parseInt(num))
}
}
class abstractHtmlAttributeParser extends ParserBackedParticle {
buildHtml() {
return ""
}
}
class linkTargetParser extends abstractHtmlAttributeParser {
get cueAtom() {
return this.getAtom(0)
}
get codeAtom() {
return this.getAtom(1)
}
}
class blankLineParser extends ParserBackedParticle {
get blankAtom() {
return this.getAtom(0)
}
get isPopular() { return true }
buildHtml() {
return this.parent.clearSectionStack()
}
}
class scrollFileAddressParser extends ParserBackedParticle {
createParserPool() {
return new Particle.ParserPool(scrollFileAddressParser, undefined, undefined)
}
get filePathAtom() {
return this.getAtomsFrom(0)
}
}
class chatLineParser extends ParserBackedParticle {
createParserPool() {
return new Particle.ParserPool(chatLineParser, undefined, undefined)
}
get stringAtom() {
return this.getAtomsFrom(0)
}
}
class lineOfCodeParser extends ParserBackedParticle {
createParserPool() {
return new Particle.ParserPool(lineOfCodeParser, undefined, undefined)
}
get codeAtom() {
return this.getAtomsFrom(0)
}
}
class commentLineParser extends ParserBackedParticle {
get commentAtom() {
return this.getAtomsFrom(0)
}
}
class cssLineParser extends ParserBackedParticle {
createParserPool() {
return new Particle.ParserPool(cssLineParser, undefined, undefined)
}
get cssAnyAtom() {
return this.getAtomsFrom(0)
}
}
class abstractTableTransformParser extends ParserBackedParticle {
createParserPool() {
return new Particle.ParserPool(undefined, Object.assign(Object.assign({}, super.createParserPool()._getCueMapAsObject()), {"#" : h1Parser,
"##" : h2Parser,
"?" : scrollQuestionParser,
"heatrix" : heatrixParser,
"heatrixAdvanced" : heatrixAdvancedParser,
"map" : mapParser,
"scatterplot" : plotScatterplotParser,
"barchart" : plotBarchartParser,
"linechart" : plotLineChartParser,
"sparkline" : sparklineParser,
"printColumn" : printColumnParser,
"printTable" : printTableParser,
"//" : slashCommentParser,
"br" : scrollBrParser,
"splitYear" : scrollSplitYearParser,
"splitDayName" : scrollSplitDayNameParser,
"splitMonthName" : scrollSplitMonthNameParser,
"splitMonth" : scrollSplitMonthParser,
"splitDayOfMonth" : scrollSplitDayOfMonthParser,
"splitDay" : scrollSplitDayOfWeekParser,
"parseDate" : scrollParseDateParser,
"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,
"assertRowCount" : assertRowCountParser,
"rename" : scrollRenameParser,
"summarize" : scrollSummarizeParser}), [{regex: /^, parser: htmlInlineParser}])
}
get cueAtom() {
return this.getAtom(0)
}
get coreTable() {
return this.parent.coreTable
}
get columnNames() {
return this.parent.columnNames
}
connectColumnNames(userColumnNames, availableColumnNames = this.parent.columnNames) {
const result = {}
const normalize = str => str.toLowerCase().trim()
userColumnNames.forEach(userColumn => {
// Strategy 1: Exact match
const exactMatch = availableColumnNames.find(col => col === userColumn)
if (exactMatch) {
result[userColumn] = exactMatch
return
}
// Strategy 2: Case-insensitive match
const normalizedUserColumn = normalize(userColumn)
const caseInsensitiveMatch = availableColumnNames.find(col => normalize(col) === normalizedUserColumn)
if (caseInsensitiveMatch) {
result[userColumn] = caseInsensitiveMatch
return
}
// Strategy 3: Levenshtein distance match
const THRESHOLD = 2 // Consider matches with distance <= 2 as "very close"
let bestMatch = null
let bestDistance = Infinity
availableColumnNames.forEach(col => {
const distance = this.root.levenshteinDistance(userColumn, col)
if (distance < bestDistance) {
bestDistance = distance
bestMatch = col
}
})
// Only use Levenshtein match if it's very close
if (bestDistance <= THRESHOLD) {
result[userColumn] = bestMatch
return
}
// Strategy 4: Fallback - use original unmatched name
result[userColumn] = userColumn
})
return result
}
connectColumnName(name) {
return this.connectColumnNames([name])[name]
}
getErrors() {
if (this.silenceErrors) return [] // todo: generalize this?
return super.getErrors()
}
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 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 dateFormat() { return `YYYY` }
get newColumnName() { return `year` }
}
class scrollSplitDayNameParser extends abstractDateSplitTransformParser {
get dateFormat() { return `dddd` }
get newColumnName() { return `dayName` }
}
class scrollSplitMonthNameParser extends abstractDateSplitTransformParser {
get dateFormat() { return `MMMM` }
get newColumnName() { return `monthName` }
}
class scrollSplitMonthParser extends abstractDateSplitTransformParser {
get dateFormat() { return `M` }
get newColumnName() { return `month` }
}
class scrollSplitDayOfMonthParser extends abstractDateSplitTransformParser {
get dateFormat() { return `D` }
get newColumnName() { return `dayOfMonth` }
}
class scrollSplitDayOfWeekParser extends abstractDateSplitTransformParser {
get dateFormat() { return `d` }
get newColumnName() { return `day` }
}
class scrollParseDateParser extends abstractTableTransformParser {
createParserPool() {class formatParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get stringAtom() {
return this.getAtom(1)
}
}
return new Particle.ParserPool(undefined, Object.assign(Object.assign({}, super.createParserPool()._getCueMapAsObject()), {"format" : formatParser}), undefined)
}
get cueAtom() {
return this.getAtom(0)
}
get columnNameAtom() {
return this.getAtom(1)
}
get coreTable() {
const columnName = this.connectColumnName(this.getAtom(1))
const formatOut = this.get("format") || "YYYY-MM-DD"
const {dayjs} = this.root
return this.parent.coreTable.map(row => {
const newRow = {...row}
try {
const value = row[columnName]
if (value) {
const date = dayjs(value)
if (date.isValid())
newRow[columnName] = date.format(formatOut)
}
} catch (err) {
console.error(`Error parsing date in column ${columnName}:`, err)
}
return newRow
})
}
}
class scrollGroupByParser extends abstractTableTransformParser {
createParserPool() {class reduceParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get columnNameAtom() {
return this.getAtom(1)
}
get reductionTypeAtom() {
return this.getAtom(2)
}
get newColumnNameAtom() {
return this.getAtom(3)
}
get reduction() {
return {
source: this.getAtom(1),
reduction: this.getAtom(2),
name: this.getAtom(3) || this.getAtomsFrom(1).join("_")
}
}
}
return new Particle.ParserPool(undefined, Object.assign(Object.assign({}, super.createParserPool()._getCueMapAsObject()), {"reduce" : reduceParser}), undefined)
}
get columnNameAtom() {
return this.getAtomsFrom(0)
}
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(particle => particle.reduction)
// 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
const newColName = col.name
if (reduction === "concat") {
newRow[newColName] = group.map((row) => row[sourceColName]).join(" ")
return
}
if (reduction === "first") {
newRow[newColName] = group.find((row) => row[sourceColName] !== "")?.[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[newColName] = 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 constantAtom() {
return this.getAtomsFrom(3)
}
get coreTable() {
// todo: use atoms here.
const columnName = this.connectColumnName(this.getAtom(1))
const operator = this.getAtom(2)
let comparisonSet
if (operator === "oneOf")
comparisonSet = new Set(this.atoms.slice(3))
let untypedComparisonValue = this.getAtom(3)
const typedComparisonValue = isNaN(parseFloat(untypedComparisonValue)) ? untypedComparisonValue : parseFloat(untypedComparisonValue)
const coreTable = this.parent.coreTable
if (!columnName || !operator || (untypedComparisonValue === undefined && !operator.includes("mpty"))) return coreTable
const filterFn = row => {
const atom = row[columnName]
const typedAtom = atom === null ? undefined : atom // convert nulls to undefined
if (operator === "=") return typedComparisonValue === typedAtom
else if (operator === "!=") return typedComparisonValue !== typedAtom
else if (operator === "includes") return typedAtom !== undefined && typedAtom.includes(typedComparisonValue)
else if (operator === "startsWith") return typedAtom !== undefined && typedAtom.toString().startsWith(typedComparisonValue)
else if (operator === "endsWith") return typedAtom !== undefined && typedAtom.toString().endsWith(typedComparisonValue)
else if (operator === "doesNotInclude") return typedAtom === undefined || !typedAtom.includes(typedComparisonValue)
else if (operator === "oneOf") return typedAtom !== undefined && comparisonSet.has(typedAtom.toString())
else if (operator === ">") return typedAtom > typedComparisonValue
else if (operator === "<") return typedAtom < typedComparisonValue
else if (operator === ">=") return typedAtom >= typedComparisonValue
else if (operator === "<=") return typedAtom <= typedComparisonValue
else if (operator === "empty") return typedAtom === "" || typedAtom === undefined
else if (operator === "notEmpty") return typedAtom !== "" && typedAtom !== undefined
}
return coreTable.filter(filterFn)
}
}
class scrollSelectParser extends abstractTableTransformParser {
get columnNameAtom() {
return this.getAtomsFrom(0)
}
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() {
if (!this._columnNames) {
const names = this.getAtomsFrom(1)
this._columnNamesMap = this.connectColumnNames(names)
this._columnNames = names.map(name => this._columnNamesMap[name])
}
return this._columnNames
}
}
class scrollReverseParser extends abstractTableTransformParser {
get coreTable() {
return this.parent.coreTable.slice().reverse()
}
}
class scrollComposeParser extends abstractTableTransformParser {
get cueAtom() {
return this.getAtom(0)
}
get newColumnNameAtom() {
return this.getAtom(1)
}
get codeAtom() {
return this.getAtomsFrom(2)
}
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 {
evaluate(str) {
return parseFloat(eval(str))
}
}
class scrollEvalParser extends scrollComputeParser {
evaluate(str) {
return eval(str)
}
}
class scrollRankParser extends scrollComposeParser {
get cueAtom() {
return this.getAtom(0)
}
get newColumnName() { return `rank` }
evaluate(str, index) { return index + 1 }
}
class scrollLinksParser extends abstractTableTransformParser {
get columnNameAtom() {
return this.getAtomsFrom(0)
}
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 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 coreTable() {
// Create a copy of the datatable 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 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 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.connectColumnName(this.getAtom(1))
}
}
class scrollOrderByParser extends abstractTableTransformParser {
get columnNameAtom() {
return this.getAtomsFrom(0)
}
get coreTable() {
const makeLodashOrderByParams = str => {
const part1 = str.split(" ")
const part2 = part1.map(col => (col.startsWith("-") ? "desc" : "asc"))
return [part1.map(col => this.connectColumnName(col.replace(/^\-/, ""))), part2]
}
const orderBy = makeLodashOrderByParams(this.content)
return this.root.lodash.orderBy(this.parent.coreTable.slice(), orderBy[0], orderBy[1])
}
}
class assertRowCountParser extends abstractTableTransformParser {
get cueAtom() {
return this.getAtom(0)
}
get integerAtom() {
return parseInt(this.getAtom(1))
}
getErrors() {
const errors = super.getErrors()
const actualRows = this.coreTable.length
const expectedRows = parseInt(this.content)
if (actualRows !== expectedRows)
return errors.concat(this.makeError(`Expected '${expectedRows}' rows but got '${actualRows}'.`))
return errors
}
}
class scrollRenameParser extends abstractTableTransformParser {
get newColumnNameAtom() {
return this.getAtomsFrom(0)
}
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 scrollSummarizeParser extends abstractTableTransformParser {
get coreTable() {
const {lodash} = this.root
const sourceData = this.parent.coreTable
if (!sourceData.length) return []
return this.parent.columnNames.map(colName => {
const values = sourceData.map(row => row[colName]).filter(val => val !== undefined && val !== null)
const numericValues = values.filter(val => typeof val === "number" && !isNaN(val))
const sorted = [...numericValues].sort((a, b) => a - b)
// Calculate mode
const frequency = {}
values.forEach(val => {
frequency[val] = (frequency[val] || 0) + 1
})
const mode = Object.entries(frequency)
.sort((a, b) => b[1] - a[1])
.map(entry => entry[0])[0]
// Calculate median for numeric values
const median = sorted.length ?
sorted.length % 2 === 0
? (sorted[sorted.length/2 - 1] + sorted[sorted.length/2]) / 2
: sorted[Math.floor(sorted.length/2)]
: null
const sum = numericValues.length ? numericValues.reduce((a, b) => a + b, 0) : null
const theType = typeof values[0]
const count = values.length
const mean = theType === "number" ? sum/count : ""
return {
name: colName,
type: theType,
incompleteCount: sourceData.length - values.length,
uniqueCount: new Set(values).size,
count,
sum,
median,
mean,
min: sorted.length ? sorted[0] : null,
max: sorted.length ? sorted[sorted.length - 1] : null,
mode
}
})
}
get columnNames() {
return ["name", "type", "incompleteCount", "uniqueCount", "count", "sum", "median", "mean", "min", "max", "mode"]
}
}
class errorParser extends ParserBackedParticle {
getErrors() { return this._getErrorParserErrors() }
}
class hakonContentParser extends ParserBackedParticle {
get codeAtom() {
return this.getAtomsFrom(0)
}
}
class heatrixCatchAllParser extends ParserBackedParticle {
get stringAtom() {
return this.getAtomsFrom(0)
}
}
class lineOfTextParser extends ParserBackedParticle {
get stringAtom() {
return this.getAtomsFrom(0)
}
get isTextParser() { return true }
}
class htmlLineParser extends ParserBackedParticle {
createParserPool() {
return new Particle.ParserPool(htmlLineParser, undefined, undefined)
}
get htmlAnyAtom() {
return this.getAtomsFrom(0)
}
}
class openGraphParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
}
class importToFooterParser extends ParserBackedParticle {
get preBuildCommandAtom() {
return this.getAtom(0)
}
}
class scriptLineParser extends ParserBackedParticle {
createParserPool() {
return new Particle.ParserPool(scriptLineParser, undefined, undefined)
}
get javascriptAnyAtom() {
return this.getAtomsFrom(0)
}
}
class linkTitleParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get stringAtom() {
return this.getAtomsFrom(1)
}
}
class programLinkParser extends ParserBackedParticle {
get codeAtom() {
return this.getAtomsFrom(0)
}
}
class scrollMediaLoopParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
}
class scrollAutoplayParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
}
class abstractCompilerRuleParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get anyAtom() {
return this.getAtomsFrom(1)
}
}
class closeSubparticlesParser extends abstractCompilerRuleParser {
}
class indentCharacterParser extends abstractCompilerRuleParser {
}
class catchAllAtomDelimiterParser extends abstractCompilerRuleParser {
}
class openSubparticlesParser extends abstractCompilerRuleParser {
}
class stringTemplateParser extends abstractCompilerRuleParser {
}
class joinSubparticlesWithParser extends abstractCompilerRuleParser {
}
class abstractConstantParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
}
class parsersBooleanParser extends abstractConstantParser {
get cueAtom() {
return this.getAtom(0)
}
get constantIdentifierAtom() {
return this.getAtom(1)
}
get booleanAtom() {
return this.getAtomsFrom(2)
}
}
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))
}
}
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))
}
}
class parsersStringParser extends abstractConstantParser {
createParserPool() {
return new Particle.ParserPool(catchAllMultilineStringConstantParser, undefined, undefined)
}
get cueAtom() {
return this.getAtom(0)
}
get constantIdentifierAtom() {
return this.getAtom(1)
}
get stringAtom() {
return this.getAtomsFrom(2)
}
}
class abstractParserRuleParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
}
class abstractNonTerminalParserRuleParser extends abstractParserRuleParser {
}
class parsersBaseParserParser extends abstractParserRuleParser {
get cueAtom() {
return this.getAtom(0)
}
get baseParsersAtom() {
return this.getAtom(1)
}
}
class catchAllAtomTypeParser extends abstractParserRuleParser {
get cueAtom() {
return this.getAtom(0)
}
get atomTypeIdAtom() {
return this.getAtom(1)
}
}
class atomParserParser extends abstractParserRuleParser {
get cueAtom() {
return this.getAtom(0)
}
get atomParserAtom() {
return this.getAtom(1)
}
}
class catchAllParserParser extends abstractParserRuleParser {
get cueAtom() {
return this.getAtom(0)
}
get parserIdAtom() {
return this.getAtom(1)
}
}
class parsersAtomsParser extends abstractParserRuleParser {
get atomTypeIdAtom() {
return this.getAtomsFrom(0)
}
}
class parsersCompilerParser extends abstractParserRuleParser {
createParserPool() {
return new Particle.ParserPool(undefined, Object.assign(Object.assign({}, super.createParserPool()._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)
}
}
class parsersExampleParser extends abstractParserRuleParser {
createParserPool() {
return new Particle.ParserPool(catchAllExampleLineParser, undefined, undefined)
}
get exampleAnyAtom() {
return this.getAtomsFrom(0)
}
}
class extendsParserParser extends abstractParserRuleParser {
get cueAtom() {
return this.getAtom(0)
}
get parserIdAtom() {
return this.getAtom(1)
}
}
class parsersPopularityParser extends abstractParserRuleParser {
get cueAtom() {
return this.getAtom(0)
}
get floatAtom() {
return parseFloat(this.getAtom(1))
}
}
class inScopeParser extends abstractParserRuleParser {
get parserIdAtom() {
return this.getAtomsFrom(0)
}
}
class parsersJavascriptParser extends abstractParserRuleParser {
createParserPool() {
return new Particle.ParserPool(catchAllJavascriptCodeLineParser, undefined, undefined)
}
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 {
}
class parsersCueParser extends abstractParseRuleParser {
get cueAtom() {
return this.getAtom(0)
}
get stringAtom() {
return this.getAtom(1)
}
}
class cueFromIdParser extends abstractParseRuleParser {
get cueAtom() {
return this.getAtom(0)
}
}
class parsersPatternParser extends abstractParseRuleParser {
get regexAtom() {
return this.getAtomsFrom(0)
}
}
class parsersRequiredParser extends abstractParserRuleParser {
}
class abstractValidationRuleParser extends abstractParserRuleParser {
get booleanAtom() {
return this.getAtomsFrom(0)
}
}
class parsersSingleParser extends abstractValidationRuleParser {
}
class uniqueLineParser extends abstractValidationRuleParser {
}
class uniqueCueParser extends abstractValidationRuleParser {
}
class listDelimiterParser extends abstractParserRuleParser {
get stringAtom() {
return this.getAtomsFrom(0)
}
}
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)
}
}
class atomTypeDescriptionParser extends ParserBackedParticle {
get stringAtom() {
return this.getAtomsFrom(0)
}
}
class catchAllErrorParser extends ParserBackedParticle {
getErrors() { return this._getErrorParserErrors() }
}
class catchAllExampleLineParser extends ParserBackedParticle {
createParserPool() {
return new Particle.ParserPool(catchAllExampleLineParser, undefined, undefined)
}
get exampleAnyAtom() {
return this.getAtom(0)
}
get exampleAnyAtom() {
return this.getAtomsFrom(1)
}
}
class catchAllJavascriptCodeLineParser extends ParserBackedParticle {
createParserPool() {
return new Particle.ParserPool(catchAllJavascriptCodeLineParser, undefined, undefined)
}
get javascriptCodeAtom() {
return this.getAtomsFrom(0)
}
}
class catchAllMultilineStringConstantParser extends ParserBackedParticle {
createParserPool() {
return new Particle.ParserPool(catchAllMultilineStringConstantParser, undefined, undefined)
}
get stringAtom() {
return this.getAtom(0)
}
get stringAtom() {
return this.getAtomsFrom(1)
}
}
class atomTypeDefinitionParser extends ParserBackedParticle {
createParserPool() {
return new Particle.ParserPool(undefined, Object.assign(Object.assign({}, super.createParserPool()._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)
}
buildHtml() {return ""}
}
class enumFromAtomTypesParser extends ParserBackedParticle {
get atomPropertyNameAtom() {
return this.getAtom(0)
}
get atomTypeIdAtom() {
return this.getAtomsFrom(1)
}
}
class parsersEnumParser extends ParserBackedParticle {
get atomPropertyNameAtom() {
return this.getAtom(0)
}
get enumOptionAtom() {
return this.getAtomsFrom(1)
}
}
class parsersExamplesParser extends ParserBackedParticle {
get atomPropertyNameAtom() {
return this.getAtom(0)
}
get atomExampleAtom() {
return this.getAtomsFrom(1)
}
}
class atomMinParser extends ParserBackedParticle {
get atomPropertyNameAtom() {
return this.getAtom(0)
}
get numberAtom() {
return parseFloat(this.getAtom(1))
}
}
class atomMaxParser extends ParserBackedParticle {
get atomPropertyNameAtom() {
return this.getAtom(0)
}
get numberAtom() {
return parseFloat(this.getAtom(1))
}
}
class parsersPaintParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get paintTypeAtom() {
return this.getAtom(1)
}
}
class parserDefinitionParser extends ParserBackedParticle {
createParserPool() {
return new Particle.ParserPool(catchAllErrorParser, Object.assign(Object.assign({}, super.createParserPool()._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)
}
buildHtml() { return ""}
}
class parsersRegexParser extends ParserBackedParticle {
get atomPropertyNameAtom() {
return this.getAtom(0)
}
get regexAtom() {
return this.getAtomsFrom(1)
}
}
class reservedAtomsParser extends ParserBackedParticle {
get atomPropertyNameAtom() {
return this.getAtom(0)
}
get reservedAtomAtom() {
return this.getAtomsFrom(1)
}
}
class extendsAtomTypeParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get atomTypeIdAtom() {
return this.getAtom(1)
}
}
class abstractColumnNameParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get columnNameAtom() {
return this.getAtom(1)
}
getRunTimeEnumOptions(atom) {
if (atom.atomTypeId === "columnNameAtom")
return this.parent.columnNames
return super.getRunTimeEnumOptions(atom)
}
}
class scrollRadiusParser extends abstractColumnNameParser {
}
class scrollSymbolParser extends abstractColumnNameParser {
}
class abstractColumnNameOrColorParser extends abstractColumnNameParser {
get cueAtom() {
return this.getAtom(0)
}
get columnNameOrColorAtom() {
return this.getAtom(1)
}
//todo: cleanup
colorOptions = "aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen".split(" ")
getRunTimeEnumOptions(atom) {
if (atom.atomTypeId === "columnNameOrColorAtom")
return this.parent.columnNames.join(this.colorOptions)
return super.getRunTimeEnumOptions(atom)
}
}
class scrollFillParser extends abstractColumnNameOrColorParser {
}
class scrollStrokeParser extends abstractColumnNameOrColorParser {
}
class scrollLabelParser extends abstractColumnNameParser {
}
class scrollSortParser extends abstractColumnNameParser {
}
class scrollXParser extends abstractColumnNameParser {
}
class scrollYParser extends abstractColumnNameParser {
}
class abstractPlotLabelParser extends ParserBackedParticle {
get cueAtom() {
return this.getAtom(0)
}
get stringAtom() {
return this.getAtomsFrom(1)
}
}
class quoteLineParser extends ParserBackedParticle {
createParserPool() {
return new Particle.ParserPool(quoteLineParser, undefined, undefined)
}
get stringAtom() {
return this.getAtomsFrom(0)
}
}
class scrollParser extends ParserBackedParticle {
createParserPool() {
return new Particle.ParserPool(catchAllParagraphParser, Object.assign(Object.assign({}, super.createParserPool()._getCueMapAsObject()), {"scrollParagraph" : scrollParagraphParser,
"authors" : authorsParser,
"blink" : blinkParser,
"scrollButton" : scrollButtonParser,
"catchAllParagraph" : catchAllParagraphParser,
"center" : scrollCenterParser,
"[]" : checklistTodoParser,
"[x]" : checklistDoneParser,
"section" : htmlSectionParser,
"header" : htmlHeaderParser,
"footer" : htmlFooterParser,
"aside" : htmlAsideParser,
"article" : htmlArticleParser,
"main" : htmlMainParser,
"nav" : htmlNavParser,
"pre" : htmlPreParser,
"ul" : htmlUlParser,
"ol" : htmlOlParser,
"li" : htmlLiParser,
"img" : htmlImgParser,
"a" : htmlAParser,
"form" : htmlFormParser,
"input" : htmlInputParser,
"select" : htmlSelectParser,
"option" : htmlOptionParser,
"textarea" : htmlTextareaParser,
"button" : htmlButtonParser,
"label" : htmlLabelParser,
"span" : htmlSpanParser,
"canvas" : htmlCanvasParser,
"iframe" : htmlIframeParser,
"h1" : h1LiteralParser,
"h2" : h2LiteralParser,
"h3" : h3LiteralParser,
"h4" : h4LiteralParser,
"kbd" : htmlKbdParser,
"meta" : htmlMetaTagParser,
"div" : htmlDivParser,
"strong" : htmlStrongParser,
"em" : htmlEmParser,
"blockquote" : htmlBlockquoteParser,
"table" : htmlTableParser,
"tr" : htmlTrParser,
"td" : htmlTdParser,
"th" : htmlThParser,
"thead" : htmlTheadParser,
"tbody" : htmlTbodyParser,
"-" : listAftertextParser,
"p" : pParagraphParser,
">" : 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,
"debugSourceStack" : debugSourceStackParser,
"---" : 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,
"modal" : scrollModalParser,
"printSnippets" : printSnippetsParser,
"fileNav" : scrollNavParser,
"printFullSnippets" : printFullSnippetsParser,
"printShortSnippets" : printShortSnippetsParser,
"printRelated" : printRelatedParser,
"notices" : scrollNoticesParser,
"assertHtmlEquals" : assertHtmlEqualsParser,
"assertBuildIncludes" : assertBuildIncludesParser,
"assertHtmlIncludes" : assertHtmlIncludesParser,
"assertHtmlExcludes" : assertHtmlExcludesParser,
"assertSilenceBelowErrors" : assertSilenceBelowErrorsParser,
"printAuthors" : printAuthorsParser,
"printDate" : printDateParser,
"printFormatLinks" : printFormatLinksParser,
"buildParsers" : buildParsersParser,
"buildCsv" : buildCsvParser,
"buildTsv" : buildTsvParser,
"buildJson" : buildJsonParser,
"buildCss" : buildCssParser,
"buildHtml" : buildHtmlParser,
"buildJs" : buildJsParser,
"buildRss" : buildRssParser,
"buildTxt" : buildTxtParser,
"loadConcepts" : loadConceptsParser,
"buildConcepts" : buildConceptsParser,
"fetch" : fetchParser,
"buildMeasures" : buildMeasuresParser,
"buildPdf" : buildPdfParser,
"inlineCss" : scrollInlineCssParser,
"inlineJs" : scrollInlineJsParser,
"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,
"keywords" : openGraphKeywordsParser,
"permalink" : permalinkParser,
"tags" : scrollTagsParser,
"title" : scrollTitleParser,
"linkTitle" : scrollLinkTitleParser,
"chat" : scrollChatParser,
"datatable" : scrollTableParser,
"cloc" : clocParser,
"dependencies" : scrollDependenciesParser,
"disk" : scrollDiskParser,
"iris" : scrollIrisParser,
"sampleData" : vegaSampleDataParser,
"concepts" : scrollConceptsParser,
"posts" : scrollPostsParser,
"postsMeta" : scrollPostsMetaParser,
"printFeed" : printFeedParser,
"printSource" : printSourceParser,
"printSiteMap" : printSiteMapParser,
"code" : codeParser,
"codeWithHeader" : codeWithHeaderParser,
"codeFromFile" : codeFromFileParser,
"debugParsers" : debugParsersParser,
"debugSourceMap" : debugSourceMapParser,
"copyButtons" : copyButtonsParser,
"heatrix" : heatrixParser,
"heatrixAdvanced" : heatrixAdvancedParser,
"map" : mapParser,
"scatterplot" : plotScatterplotParser,
"barchart" : plotBarchartParser,
"linechart" : plotLineChartParser,
"sparkline" : sparklineParser,
"printColumn" : printColumnParser,
"printTable" : printTableParser,
"katex" : katexParser,
"helpfulNotFound" : helpfulNotFoundParser,
"slideshow" : slideshowParser,
"tableSearch" : tableSearchParser,
"comment" : commentParser,
"!" : counterpointParser,
"//" : slashCommentParser,
"thanksTo" : thanksToParser,
"clearStack" : scrollClearStackParser,
"css" : cssParser,
"background" : scrollBackgroundColorParser,
"color" : scrollFontColorParser,
"font" : scrollFontParser,
"dashboard" : scrollDashboardParser,
"belowAsCode" : belowAsCodeParser,
"debugBelow" : debugBelowParser,
"debugAbove" : debugAboveParser,
"debugAll" : debugAllParser,
"belowAsCodeUntil" : belowAsCodeUntilParser,
"aboveAsCode" : aboveAsCodeParser,
"belowAsHtml" : belowAsHtmlParser,
"aboveAsHtml" : aboveAsHtmlParser,
"hakon" : hakonParser,
"html" : htmlParser,
"br" : scrollBrParser,
"iframes" : iframesParser,
"image" : scrollImageParser,
"qrcode" : qrcodeParser,
"youtube" : youtubeParser,
"youTube" : youTubeParser,
"import" : importParser,
"imported" : scrollImportedParser,
"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,
"toFooter" : toFooterParser,
"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)(?:\?[^\s]*)?$/, parser: quickSoundParser},{regex: /^[^\s]+\.(mp4|webm|avi|mov)(?:\?[^\s]*)?$/, parser: quickVideoParser},{regex: /^[^\s]+\.(tsv|csv|ssv|psv|json)(?:\?[^\s]*)?$/, parser: quickTableParser},{regex: /^[a-zA-Z0-9_]+Code$/, parser: codeWithLanguageParser},{regex: /^[^\s]+\.(css)(?:\?[^\s]*)?$/, parser: quickCssParser},{regex: /^[^\s]+\.(html|htm)(?:\?[^\s]*)?$/, parser: quickIncludeHtmlParser},{regex: /^[^\s]+\.(js)(?:\?[^\s]*)?$/, parser: quickScriptParser},{regex: /^[^\s]+\.(mjs)(?:\?[^\s]*)?$/, parser: quickModuleScriptParser},{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)(?:\?[^\s]*)?$/, parser: quickImageParser},{regex: /^[^\s]+\.(scroll|parsers)$/, parser: quickImportParser},{regex: /^[^\s]+\.(py|pl|sh|rb|php)$/, parser: quickRunScriptParser},{regex: /^$/, parser: blankLineParser},{regex: /^[a-zA-Z0-9_]+Atom$/, parser: atomTypeDefinitionParser},{regex: /^[a-zA-Z0-9_]+Parser$/, parser: parserDefinitionParser}])
}
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 this.getInBrowser(fileOrUrl)
const isUrl = fileOrUrl.match(/^https?\:[^ ]+$/)
if (!isUrl) return this.root.readFile(fileOrUrl)
return this.readFile(this.makeFullPath(new URL(fileOrUrl).pathname.split('/').pop()))
}
getInBrowser(key) {
return localStorage.getItem(key) || (window.inMemStorage ? window.inMemStorage[key] : "") || ""
}
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 = this.getInBrowser(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()
const text = await blob.text()
try {
localStorage.setItem(url, text)
return localStorage.getItem(url)
} catch (err) {
if (!window.inMemStorage) window.inMemStorage = {}
window.inMemStorage[url] = text
console.error(err)
return text
}
}
readFile(filename) {
const {path} = this
const fs = require("fs")
const fullPath = path.join(this.folderPath, filename.replace(this.folderPath, ""))
try {
if (fs.existsSync(fullPath))
return fs.readFileSync(fullPath, "utf8")
console.error(`File '${filename}' not found`)
return ""
} catch (err) {
console.error(`Error in '${this.filePath}' reading file: '${fullPath}'`)
console.error(err)
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 "170.4.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 keywords() {
if (this.has("keywords"))
return this.get("keywords")
const tags = this.get("tags")
if (tags)
return tags.split(" ").join(", ")
return ""
}
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(/\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
}
get d3() {
return this.isNodeJs() ? require('d3') : d3
}
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(options = {}) {
await this.load()
await this.buildOne(options)
await this.buildTwo(options)
}
async buildOne(options) {
await this.execute()
const toBuild = this.filter(particle => particle.buildOne)
for (let particle of toBuild) {
await particle.buildOne(options)
}
}
async buildTwo(options) {
const toBuild = this.filter(particle => particle.buildTwo)
for (let particle of toBuild) {
await particle.buildTwo(options)
}
}
get outputFileNames() {
return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat()
}
_compileArray(filename, arr) {
const removeBlanks = data => data.map(obj => Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== "")))
const parts = filename.split(".")
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()
}
levenshteinDistance(a, b) {
const m = a.length
const n = b.length
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))
for (let i = 0; i <= m; i++) {
dp[i][0] = i
}
for (let j = 0; j <= n; j++) {
dp[0][j] = j
}
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost)
}
}
return dp[m][n]
}
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) || fieldValue.includes('"'))) {
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|toFooter$)/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
try {
if (kind === "replaceJs") value = eval(value)
if (this.isNodeJs() && kind === "replaceNodejs")
this.evalNodeJsMacros(value, macroMap, absolutePath)
else macroMap[particle.getAtom(1)] = value
} catch (err) {
console.error(err)
}
particle.destroy() // Destroy definitions after eval
})
if (particle.has("toFooter")) {
const pushes = particle.getParticles("toFooter")
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
paint constant
newColumnNameAtom
description Name a derived column.
paint variable
constantAtom
paint constant
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 oneOf
paint constant
personNameAtom
extends stringAtom
urlAtom
paint constant.language
absoluteUrlAtom
paint constant.language
regex (ftp|https?)://.+
emailAddressAtom
extends stringAtom
paint constant.language
permalinkAtom
paint string
description A string that doesn't contain characters that might interfere with most filesystems. No slashes, for instance.
filePathAtom
paint constant.language
tagOrUrlAtom
description An HTML tag or a url.
paint constant.language
htmlAttributesAtom
paint constant
htmlMethodAtom
paint constant
enum get post
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
htmlTypeAtom
extends stringAtom
paint constant.language
enum text password email number tel url search date time datetime-local week month color checkbox radio file submit reset button hidden range
description HTML input type attribute values.
classNameAtom
paint constant
htmlIdAtom
paint constant
fontFamilyAtom
enum Arial Helvetica Verdana Georgia Impact Tahoma Slim
paint constant
javascriptAnyAtom
extends codeAtom
htmlAnyAtom
extends codeAtom
colorAtom
extends codeAtom
namedColorAtom
extends colorAtom
enum aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgray lightgreen lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose moccasin navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple rebeccapurple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen
mimeTypeAtom
extends stringAtom
paint constant.language
description File MIME types (e.g. 'image/*', 'application/pdf')
enum image/* image/jpeg image/png image/gif image/webp image/svg+xml application/pdf text/plain text/html text/css text/javascript text/csv application/json application/xml application/zip application/msword application/vnd.openxmlformats-officedocument.wordprocessingml.document application/vnd.ms-excel application/vnd.openxmlformats-officedocument.spreadsheetml.sheet audio/* audio/mpeg audio/wav audio/ogg video/* video/mp4 video/webm video/ogg
buildCommandAtom
extends cueAtom
description Give build command atoms their own color.
paint constant
cssAnyAtom
extends codeAtom
cssLengthAtom
extends codeAtom
reductionTypeAtom
enum sum mean max min concat first
paint keyword
inlineMarkupNameAtom
description Options to turn on some inline markups.
enum bold italics code katex none
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 cueAtom 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 constant
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
columnNameOrColorAtom
extends columnNameAtom
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
vegaDataSetAtom
paint constant.numeric
enum airports.csv anscombe.json barley.json birdstrikes.json budget.json budgets.json burtin.json cars.json climate.json co2-concentration.csv countries.json crimea.json descriptions.json disasters.csv driving.json earthquakes.json flare-dependencies.json flare.json flights-10k.json flights-200k.json flights-20k.json flights-2k.json flights-3m.csv flights-5k.json flights-airport.csv gapminder-health-income.csv gapminder.json github.csv graticule.json income.json iowa-electricity.csv iris.json jobs.json la-riots.csv londonBoroughs.json londonCentroids.json londonTubeLines.json lookup_groups.csv lookup_people.csv miserables.json monarchs.json movies.json normal-2d.json obesity.json points.json population.json population_engineers_hurricanes.csv seattle-temps.csv seattle-weather.csv sf-temps.csv sp500.csv stocks.csv udistrict.json unemployment-across-industries.json unemployment.tsv us-10m.json us-employment.csv us-state-capitals.json weather.csv weather.json weball26.json wheat.json windvectors.csv world-110m.json zipcodes.csv
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"
}
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
}
get tag() {return "p"}
get className() {
if (this.get("class"))
return this.get("class")
const classLine = this.getParticle("addClass")
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 selfClose = this.isSelfClosing ? " /" : ""
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}\${selfClose}>\${this.htmlContents}\${this.closingTag}\`
}
get htmlContents() {
return this.text
}
get closingTag() {
if (this.isSelfClosing) return ""
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
}
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 ") : "")
}
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"
blinkParser
description Just for fun.
extends scrollParagraphParser
example
blink Carpe diem!
cue blink
javascript
buildHtml() {
return \`\${super.buildHtml()}
\`
}
scrollButtonParser
extends scrollParagraphParser
cueFromId
description A button.
postParser
description Post a particle.
example
scrollButton Click me
javascript
defaultClassName = "scrollButton"
tag = "button"
get htmlAttributes() {
const link = this.getFromParser("scrollLinkParser")
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))
}
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() || ""
}
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
}
abstractIndentableParagraphParser
extends scrollParagraphParser
inScope abstractAftertextDirectiveParser abstractAftertextAttributeParser abstractIndentableParagraphParser scrollParagraphParser
javascript
get htmlContents() {
return this.text + this.map(particle => particle.buildHtml())
.join("\\n")
.trim()
}
buildTxt() {
return this.getAtom(0) + " " + super.buildTxt()
}
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()
}
checklistDoneParser
popularity 0.000072
extends checklistTodoParser
description A completed task.
string checked checked
cue [x]
example
[x] get milk
abstractHtmlElementParser
description HTML tag.
extends abstractIndentableParagraphParser
javascript
defaultClassName = ""
get tag() { return this.cue}
buildTxt() {
return ""
}
htmlSectionParser
extends abstractHtmlElementParser
cue section
example
section
# Hello world
p This is a test
section
# Deeper
htmlHeaderParser
extends abstractHtmlElementParser
cue header
example
header
# Title
htmlFooterParser
extends abstractHtmlElementParser
cue footer
example
footer
p Made with love
htmlAsideParser
extends abstractHtmlElementParser
cue aside
example
aside
h1 Some notes
htmlArticleParser
extends abstractHtmlElementParser
cue article
example
article
h1 My article
htmlMainParser
extends abstractHtmlElementParser
cue main
example
main
# Title
htmlNavParser
extends abstractHtmlElementParser
cue nav
example
nav
a Home
href /
htmlPreParser
extends abstractHtmlElementParser
cue pre
example
pre
htmlUlParser
extends abstractHtmlElementParser
cue ul
example
ul
li A list
htmlOlParser
extends abstractHtmlElementParser
cue ol
example
ol
li A list
htmlLiParser
extends abstractHtmlElementParser
cue li
example
ol
li A list
htmlImgParser
extends abstractHtmlElementParser
cue img
boolean isSelfClosing true
example
img foo.png
htmlAParser
extends abstractHtmlElementParser
cue a
example
a Home
href /
htmlFormParser
extends abstractHtmlElementParser
cue form
example
form
input
htmlInputParser
extends abstractHtmlElementParser
cue input
boolean isSelfClosing true
example
input
type text
placeholder Enter your name
htmlSelectParser
extends abstractHtmlElementParser
cue select
example
select
option Value 1
option Value 2
htmlOptionParser
extends abstractHtmlElementParser
cue option
example
select
option Choose an option
option First Option
htmlTextareaParser
extends abstractHtmlElementParser
cue textarea
example
textarea
placeholder Enter your message
rows 4
htmlButtonParser
extends abstractHtmlElementParser
cue button
example
button Submit
type submit
htmlLabelParser
extends abstractHtmlElementParser
cue label
example
label Name
for username
htmlSpanParser
extends abstractHtmlElementParser
cue span
example
span Hello
htmlCanvasParser
extends abstractHtmlElementParser
cue canvas
example
canvas
htmlIframeParser
extends abstractHtmlElementParser
cue iframe
example
iframe
h1LiteralParser
extends abstractHtmlElementParser
cue h1
example
main
h1 Title
h2LiteralParser
extends abstractHtmlElementParser
cue h2
example
main
h2 Title
h3LiteralParser
extends abstractHtmlElementParser
cue h3
example
main
h3 Title
h4LiteralParser
extends abstractHtmlElementParser
cue h4
example
main
h4 Title
htmlKbdParser
extends abstractHtmlElementParser
cue kbd
example
main
kbd Ctrl+→
htmlMetaTagParser
extends abstractHtmlElementParser
cue meta
boolean isSelfClosing true
example
meta
name description
content A great page about meta tags
htmlDivParser
extends abstractHtmlElementParser
cue div
example
div
# Hello world
div
# Deeper
htmlStrongParser
extends abstractHtmlElementParser
cue strong
example
strong Important text
htmlEmParser
extends abstractHtmlElementParser
cue em
example
em Emphasized text
htmlBlockquoteParser
extends abstractHtmlElementParser
cue blockquote
example
blockquote
p A wise quote
htmlTableParser
extends abstractHtmlElementParser
cue table
example
table
tr
th Header 1
th Header 2
tr
td Cell 1
td Cell 2
htmlTrParser
extends abstractHtmlElementParser
cue tr
example
tr
td Cell 1
htmlTdParser
extends abstractHtmlElementParser
cue td
example
td Cell content
htmlThParser
extends abstractHtmlElementParser
cue th
example
th Header content
htmlTheadParser
extends abstractHtmlElementParser
cue thead
example
thead
tr
th Header 1
htmlTbodyParser
extends abstractHtmlElementParser
cue tbody
example
tbody
tr
td Cell 1
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"
abstractCustomListItemParser
extends listAftertextParser
javascript
get requireOnce() {
return \`\`
}
get attributes() {
return \`class="\${this.constructor.name}"\`
}
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)}"\`}
pParagraphParser
popularity 0.001881
cue p
extends abstractIndentableParagraphParser
description A paragraph.
boolean isArticleContent true
example
p I had a _new_ idea.
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"
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
}
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 = ""
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()
}
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
h1Parser
popularity 0.017918
description An html h1 tag.
extends abstractHeaderParser
example
# Hello world
boolean isArticleContent true
cue #
boolean isPopular true
javascript
tag = "h1"
h2Parser
popularity 0.005257
description An html h2 tag.
extends abstractHeaderParser
example
## Hello world
boolean isArticleContent true
cue ##
boolean isPopular true
javascript
tag = "h2"
h3Parser
popularity 0.001085
description An html h3 tag.
extends abstractHeaderParser
example
### Hello world
boolean isArticleContent true
cue ###
javascript
tag = "h3"
h4Parser
popularity 0.000289
description An html h4 tag.
example
#### Hello world
extends abstractHeaderParser
cue ####
javascript
tag = "h4"
scrollQuestionParser
popularity 0.004244
description A question.
extends h4Parser
cue ?
example
? Why is the sky blue?
javascript
defaultClassName = "scrollQuestion"
h5Parser
description An html h5 tag.
extends abstractHeaderParser
example
##### Hello world
cue #####
javascript
tag = "h5"
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"
captionAftertextParser
popularity 0.003207
description An image caption.
cue caption
example
map.png
caption A map.
extends scrollParagraphParser
boolean isPopular true
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}>\`
}
scrollMusicParser
popularity 0.000024
extends abstractMediaParser
cue music
description Play sound files.
example
music sipOfCoffee.m4a
javascript
buildHtml() {
return \`\`
}
quickSoundParser
popularity 0.000024
extends scrollMusicParser
atoms urlAtom
pattern ^[^\\s]+\\.(mp3|wav|ogg|aac|m4a|flac)(?:\\?[^\\s]*)?$
int atomIndex 0
example
sipOfCoffee.m4a
scrollVideoParser
popularity 0.000024
extends abstractMediaParser
cue video
example
video spirit.mp4
description Play video files.
widthParser
cueFromId
atoms cueAtom integerAtom
heightParser
cueFromId
atoms cueAtom integerAtom
javascript
tag = "video"
quickVideoParser
popularity 0.000024
extends scrollVideoParser
example
spirit.mp4
atoms urlAtom
pattern ^[^\\s]+\\.(mp4|webm|avi|mov)(?:\\?[^\\s]*)?$
int atomIndex 0
quickParagraphParser
popularity 0.001881
cue *
extends scrollParagraphParser
description A paragraph.
boolean isArticleContent true
example
* I had a _new_ idea.
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
}
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 + \`
\`
}
editLinkParser
popularity 0.001206
extends abstractTextLinkParser
description Print "Edit" link.
string text Edit
javascript
get link() {
return this.root.editUrl || ""
}
scrollVersionLinkParser
popularity 0.006294
extends abstractTextLinkParser
string link https://scroll.pub
description Print Scroll version.
javascript
get text() {
return \`Built with Scroll v\${this.root.scrollVersion}\`
}
classicFormParser
cue classicForm
popularity 0.006391
description Generate input form for ScrollSet.
extends abstractAftertextParser
atoms cueAtom
catchAllAtomType stringAtom
string script
string style
javascript
get inputs() {
return this.root.measures.filter(measure => !measure.IsComputed).map((measure, index) => {
const {Name, Question, IsRequired, Type} = measure
const type = Type || "text"
const placeholder = Question
const ucFirst = Name.substr(0, 1).toUpperCase() + Name.substr(1)
// \${index ? "" : "autofocus"}
let tag = ""
if (Type === "textarea")
tag = \`\`
else
tag = \`\`
return \`
\${tag}
\`
}).join("\\n")
}
buildHtml() {
const {isEmail, formDestination, callToAction, subject} = this
return \`\${this.script}\${this.style}\`
}
get callToAction() {
return (this.isEmail ? "Submit via email" : (this.subject || "Post"))
}
get isEmail() {
return this.formDestination.includes("@")
}
get formDestination() {
return this.getAtom(1) || ""
}
get subject() {
return this.getAtomsFrom(2)?.join(" ") || ""
}
get footer() {
return ""
}
scrollFormParser
extends classicFormParser
cue scrollForm
placeholderParser
atoms cueAtom
baseParser blobParser
cueFromId
single
valueParser
atoms cueAtom
baseParser blobParser
cueFromId
single
nameParser
description Name for the post submission.
atoms cueAtom stringAtom
cueFromId
single
description Generate a Scroll Form.
string copyFromExternal .codeMirror.css .scrollLibs.js .constants.js
string requireOnce
javascript
get placeholder() {
return this.getParticle("placeholder")?.subparticlesToString() || ""
}
get value() {
return this.getParticle("value")?.subparticlesToString() || ""
}
get footer() {
return ""
}
get name() {
return this.get("name") || "particles"
}
get parsersBundle() {
const parserRegex = /^[a-zA-Z0-9_]+Parser$/gm
const clone = this.root.clone()
const parsers = clone.filter(line => parserRegex.test(line.getLine()))
return "\\n" + parsers.map(particle => {
particle.prependLine("boolean suggestInAutocomplete true")
return particle.toString()
}).join("\\n")
}
get inputs() {
const Name = this.name
return \`
\`
}
buildHtml(buildSettings) {
return this.getHtmlRequirements(buildSettings) + super.buildHtml()
}
loremIpsumParser
extends abstractAftertextParser
cueFromId
description Generate dummy text.
catchAllAtomType integerAtom
string placeholder Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
javascript
get originalText() {
return this.placeholder.repeat(this.howMany)
}
get howMany() {
return this.getAtom(1) ? parseInt(this.getAtom(1)) : 1
}
nickelbackIpsumParser
extends loremIpsumParser
string placeholder And one day, I’ll be at the door. And lose your wings to fall in love? To the bottom of every bottle. I’m on the ledge of the eighteenth story. Why must the blind always lead the blind?
scrollModalParser
description A modal dialog overlay.
extends abstractAftertextParser
boolean isHtml true
cue modal
string requireOnce
javascript
buildHtml(buildSettings) {
this.parent.sectionStack.push("")
return this.getHtmlRequirements(buildSettings) + \`
\`
}
printSnippetsParser
popularity 0.000338
// todo: why are we extending AT here and not loops? Is it for class/id etc?
extends abstractAftertextParser
cueFromId
atoms cueAtom
catchAllAtomType tagWithOptionalFolderAtom
description Prints snippets matching tag(s).
example
printSnippets index
javascript
makeSnippet(scrollProgram, buildSettings) {
const {endSnippetIndex} = scrollProgram
if (endSnippetIndex === -1) return scrollProgram.buildHtmlSnippet(buildSettings) + scrollProgram.editHtml
const linkRelativeToCompileTarget = buildSettings.relativePath + scrollProgram.permalink
const joinChar = "\\n"
const html = scrollProgram
.map((subparticle, index) => (index >= endSnippetIndex ? "" : subparticle.buildHtmlSnippet ? subparticle.buildHtmlSnippet(buildSettings) : subparticle.buildHtml(buildSettings)))
.filter(i => i)
.join(joinChar)
.trim() +
\`Continue reading...\`
return html
}
get files() {
const thisFile = this.parent.file
const files = this.root.getFilesByTags(this.content, this.has("limit") ? parseInt(this.get("limit")) : undefined).filter(file => file.file !== thisFile)
// allow sortBy lastCommit Time
if (this.get("sortBy") === "commitTime") {
return this.root.sortBy(files, file => file.scrollProgram.lastCommitTime).reverse()
}
return files
}
buildHtml() {
const alreadyRequired = this.root.alreadyRequired
const snippets = this.files.map(file => {
const buildSettings = {relativePath: file.relativePath, alreadyRequired }
return \`