diff --git a/ScrollSetCLI.js b/ScrollSetCLI.js index 3f8d769da..65fceef9f 100644 --- a/ScrollSetCLI.js +++ b/ScrollSetCLI.js @@ -6,7 +6,6 @@ const { Utils } = require("scrollsdk/products/Utils.js") const { Disk } = require("scrollsdk/products/Disk.node.js") const { ScrollCli } = require("./scroll.js") const scrollFs = new ScrollCli().sfs -const ScrollFile = scrollFs.defaultFileClass class ScrollSetCLI { constructor() { @@ -57,7 +56,7 @@ class ScrollSetCLI { } async _formatAndSave(filePath, particle) { - const fusedFile = new ScrollFile(particle.toString(), filePath, scrollFs) + const fusedFile = scrollFs.newFile(particle.toString(), filePath) await fusedFile.fuse() // force a write const result = await scrollFs.write(filePath, fusedFile.scrollProgram.formatted) diff --git a/package.json b/package.json index 9b6a81b39..135c90bc6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "scroll-cli", - "version": "176.0.1", + "version": "177.0.0", "description": "A language for scientists of all ages. A curated collection of tools for refining and sharing thoughts.", "main": "scroll.js", "engines": { @@ -53,7 +53,7 @@ "lodash": "^4.17.21", "minimist": "^1.2.5", "scroll-cli": "^170.2.0", - "scrollsdk": "^106.0.1" + "scrollsdk": "^107.0.0" }, "devDependencies": { "prettier": "^2.8.8", diff --git a/parsers/assertions.parsers b/parsers/assertions.parsers index f2208390e..b43f2a983 100644 --- a/parsers/assertions.parsers +++ b/parsers/assertions.parsers @@ -9,11 +9,11 @@ abstractAssertionParser // If the previous particle is also an assertion particle, use the one before that. return this.previous.particleToTest ? this.previous.particleToTest : this.previous } - get format() { return "html"} + get outputFormat() { return "html"} get actual() { - let {format} = this - format = format.substr(0, 1).toUpperCase() + format.substr(1) - const methodName = `build${format}` + let {outputFormat} = this + outputFormat = outputFormat.substr(0, 1).toUpperCase() + outputFormat.substr(1) + const methodName = `build${outputFormat}` return this.particleToTest[methodName]() } getErrors() { @@ -53,7 +53,7 @@ assertParser Hello assert txt includes Hello javascript - get format() { + get outputFormat() { return this.atoms[1] } get expected() { @@ -92,6 +92,18 @@ assertBuildIncludesParser } get actual() { return this.particleToTest.buildOutput()} +assertBuildEndsWithParser + extends assertBuildIncludesParser + string kind endsWith + example + buildCsv + assertBuildEndsWith virginica + javascript + areEqual(actual, expected) { + // Note that this does do a trim on both. + return actual.trim().endsWith(expected.trim()) + } + assertHtmlIncludesParser extends abstractAssertionParser string kind include diff --git a/parsers/atomTypes.parsers b/parsers/atomTypes.parsers index 0a7c07f5f..459966350 100644 --- a/parsers/atomTypes.parsers +++ b/parsers/atomTypes.parsers @@ -16,9 +16,9 @@ countAtom yearAtom extends integerAtom -preBuildCommandAtom +parseTimeCommandAtom extends cueAtom - description Give build command atoms their own color. + description Give parse time command atoms their own color. paint constant.character.escape delimiterAtom diff --git a/parsers/code.parsers b/parsers/code.parsers index a75248754..6fda85af4 100644 --- a/parsers/code.parsers +++ b/parsers/code.parsers @@ -34,10 +34,13 @@ codeWithHeaderParser two = 1 + 1 javascript buildHtml() { - return `
${this.content}
${super.buildHtml()}
` + return `
${this.header}
${super.buildHtml()}
` } buildTxt() { - return "```" + this.content + "\n" + this.code + "\n```" + return "```" + this.header + "\n" + this.code + "\n```" + } + get header() { + return this.content } codeFromFileParser diff --git a/parsers/concepts.parsers b/parsers/concepts.parsers index 93ea2f8d4..f3cce09d3 100644 --- a/parsers/concepts.parsers +++ b/parsers/concepts.parsers @@ -16,17 +16,52 @@ scrollConceptsParser loadConceptsParser // todo: clean this up. just add smarter imports with globs? - // this currently removes any "import" statements. + // this currently removes first line of each (to ditch the imports). description Import all concepts in a folder. - extends abstractBuildCommandParser + extends abstractScrollParser cueFromId - atoms preBuildCommandAtom filePathAtom + atoms parseTimeCommandAtom filePathAtom javascript - async load() { - const { Disk, path, importRegex } = this.root + async wake() { + const { Disk, path } = this.root const folder = path.join(this.root.folderPath, this.getAtom(1)) - const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(".scroll")).map(Disk.read).join("\n\n").replace(importRegex, "") - this.parent.concat(ONE_BIG_FILE) + const ONE_BIG_FILE = Disk.getFiles(folder).filter(file => file.endsWith(".scroll")).map(Disk.read).map(content => content.split("\n").slice(1).join("\n")).join("\n\n") + this.parent.concat(ONE_BIG_FILE) + } + +conceptTemplateParser + extends abstractScrollParser + description Loads a template after document is parsed. + example + conceptTemplate + # This is a page for {name} + His phone is {phone} + cueFromId + javascript + async wake() { + this.root.notifyOnReady(this) + } + onReady() { + if (this.root.has("importOnly")) return // Don't run on import only pages. + const concept = this.root.concept + const templateString = new Particle(this.subparticlesToString()) + const newCode = templateString.templateToString(concept) + this.replaceWith(newCode) + } + +dumpConceptParser + popularity 0.000169 + cueFromId + atoms cueAtom + extends codeWithHeaderParser + example + dumpConcept + javascript + get code() { + return new Particle(this.root.concept).toString() + } + get header() { + return this.root.filename } buildConceptsParser @@ -46,7 +81,7 @@ buildConceptsParser return this.get("sortBy") } get outputFiles() { - const {permalink} = this + const {permalink} = this.root const files = this.getAtomsFrom(1) if (!files.length) files.push(permalink.replace(".html", ".csv")) return files diff --git a/parsers/debug.parsers b/parsers/debug.parsers index a2feebb98..740441064 100644 --- a/parsers/debug.parsers +++ b/parsers/debug.parsers @@ -28,18 +28,13 @@ debugSourceStackParser } debugParsersParser - description Print the parsers used. + description Print source code of all parsers used. extends codeParser cueFromId javascript buildParsers() { return this.code} get code() { - let code = new Particle(this.root.definition.toString()) - // Remove comments - code.filter((line) => line.getLine().startsWith("//")).forEach((particle) => particle.destroy()) - // Remove blank lines - code = code.toString().replace(/^\n/gm, "") - return code + return this.root.definition.toString() } debugBelowParser diff --git a/parsers/dinkus.parsers b/parsers/dinkus.parsers index dffaf1ee5..680db25e8 100644 --- a/parsers/dinkus.parsers +++ b/parsers/dinkus.parsers @@ -38,6 +38,7 @@ scrollDinkusParser customDinkusParser cue dinkus description A custom dinkus. + atoms cueAtom stringAtom extends abstractDinkusParser endOfPostDinkusParser diff --git a/parsers/fetch.parsers b/parsers/fetch.parsers index 7b98655ea..2d07d092a 100644 --- a/parsers/fetch.parsers +++ b/parsers/fetch.parsers @@ -2,7 +2,7 @@ fetchParser description Download URL to disk. extends abstractBuildCommandParser cueFromId - atoms preBuildCommandAtom urlAtom + atoms parseTimeCommandAtom urlAtom example fetch https://breckyunits.com/posts.csv fetch https://breckyunits.com/posts.csv renamed.csv diff --git a/parsers/format.parsers b/parsers/format.parsers new file mode 100644 index 000000000..f78aad2b3 --- /dev/null +++ b/parsers/format.parsers @@ -0,0 +1,5 @@ +scrollNoFormatParser + description Don't format below this line. + cue noFormat + atoms parseTimeCommandAtom + extends abstractTopLevelSingleMetaParser \ No newline at end of file diff --git a/parsers/import.parsers b/parsers/import.parsers index 373ed1729..741011fbc 100644 --- a/parsers/import.parsers +++ b/parsers/import.parsers @@ -2,21 +2,112 @@ importParser description Import a file. popularity 0.007524 cueFromId - atoms preBuildCommandAtom + atoms parseTimeCommandAtom extends abstractScrollParser catchAllAtomType filePathAtom + int filenameAt 1 example import header.scroll + javascript + get filename() { + return this.getAtom(this.filenameAt) + } + get fullpath() { + const {fileSystem, filePath} = this.root + return fileSystem.makeRelativePath(filePath, this.filename) + } + async wake() { + const {fullpath} = this + const {file} = this.root + if (!file.importsFiles) + file.importsFiles = new Set() + file.importsFiles.add(fullpath) + return await this._handleImport() + } + async _handleImport() { + const hasCircular = await this._hasCircularImports(this.root.filePath, this.fullpath) + if (hasCircular) + return await this._handleCircularImport(hasCircular) + const file = await this._getFile() + const particleCount = file.scrollProgram.length + if (!file.exists) + return await this._handleFileDoesNotExist() + const newParticle = new Particle(`imported ${this.fullpath}\n exists true\n original ${this.getLine()}\n particles ${particleCount}`) + if (this.has("moveToFooter")) + newParticle.particleAt(0).set("moveToFooter", "true") + await this.root.appendFromStream(newParticle.toString()) + // Don't import importOnly lines + // todo: look at perf + const block = new Particle(file.scrollProgram.toString()) + block.delete("importOnly") + await this.root.appendFromStream(block.toString()) + this.destroy() + } + async _handleFileDoesNotExist() { + const newParticle = new Particle(`imported ${this.fullpath}\n exists false\n original ${this.getLine()}\n particles 0`) + await this.root.appendFromStream(newParticle.toString()) + this.destroy() + } + async _handleCircularImport(hasCircular) { + const newParticle = new Particle(`imported ${this.fullpath}\n exists true\n original ${this.getLine()}\n particles 0\n circularImportError Circular import error: ${hasCircular}`) + await this.root.appendFromStream(newParticle.toString()) + this.destroy() + } + async _hasCircularImports(importer, importee) { + // if a.scroll requires a.scroll + if (importer === importee) + return `${importee} imports itself` + const {fileSystem} = this.root + // Now check the files that the importee is importing + const importeeFile = await fileSystem.getFile(importee) + if (!importeeFile.importsFiles) + return false + if (importeeFile.importsFiles.has(importer)) + return `${importee} imports ${importer}` + for (let importPath of importeeFile.importsFiles) { + // console.log(`Checking if ${importPath} imports ${importer}`) + if (await this._hasCircularImports(importPath, importer)) + return `${importPath} and ${importer}` + } + return false + } + async _getFile() { + const {fileSystem, file} = this.root + const fileToImport = await fileSystem.getFile(this.fullpath) + await fileToImport.singlePassFuse() + return fileToImport + } scrollImportedParser description Inserted at import pass. boolean suggestInAutocomplete false cue imported - atoms preBuildCommandAtom + atoms parseTimeCommandAtom extends abstractScrollParser baseParser blobParser catchAllAtomType filePathAtom javascript + async wake() { + this.checkForFooterMoves() + } + get dependencies() { return [this.atoms[1]]} + checkForFooterMoves() { + // If a particle was imported with the "moveToFooter" subparticle set, then that means we want + // to move those imported particles to the end of the document. + if (!this.has("moveToFooter")) + return + this.root.notifyOnReady(this) + } + onReady() { + // We move the imported particle along with the number of lines originally imported + // to the bottom of the document. Note: I have not looked into perf yet. + const particleCount = parseInt(this.get("particles")) + const index = this.index + const particles = this.root.slice(index, index + particleCount + 1) + const str = particles.map(part => part.toString()).join("\n") + this.root.appendBlocks(str) + particles.forEach(particle => particle.destroy()) + } getErrors() { if (this.has("circularImportError")) return [this.makeError(this.get("circularImportError"))] @@ -27,26 +118,56 @@ scrollImportedParser importToFooterParser description Import to bottom of file. - atoms preBuildCommandAtom - cue footer + atoms parseTimeCommandAtom + cue moveToFooter quickImportParser popularity 0.007524 - description Import a Scroll or Parsers file. - extends abstractScrollParser + description Import a Scroll file. + extends importParser boolean isPopular true + int filenameAt 0 inScope importToFooterParser abstractCommentParser atoms urlAtom - pattern ^[^\s]+\.(scroll|parsers)$ + pattern ^[^\s]+\.scroll$ example header.scroll +useParserPoolParser + cueFromId + description Load a cached parser pool. + extends abstractScrollParser + atoms cueAtom urlAtom + javascript + async wake() { + this.switchParserPool(this.filename) + } + get filename() { return this.atoms[1]} + get dependencies() { return [this.filename]} + +parsersImportParser + description Import a Parsers file. + extends quickImportParser + inScope abstractCommentParser + pattern ^[^\s]+\.parsers$ + example + measures.parsers + javascript + async _handleImport() { + const file = await this._getFile() + if (!file.exists) + return await this._handleFileDoesNotExist() + // todo: cleanup + const newp = this.replaceWith(`useParserPool ${this.fullpath}`) + await newp[0].wake() + } + importOnlyParser popularity 0.033569 // This line will be not be imported into the importing file. description Don't build this file. cueFromId - atoms preBuildCommandAtom + atoms parseTimeCommandAtom extends abstractTopLevelSingleMetaParser abstractQuickIncludeParser diff --git a/parsers/javascript.parsers b/parsers/javascript.parsers index 1ccbb7f84..d5cfa5413 100644 --- a/parsers/javascript.parsers +++ b/parsers/javascript.parsers @@ -60,17 +60,17 @@ evalJsParser extends abstractScrollParser baseParser blobParser cueFromId - atoms preBuildCommandAtom + atoms parseTimeCommandAtom catchAllAtomType javascriptAnyAtom example evalJs "# The sum of 1 + 1 is " + 1 + 1 javascript - wake() { + async wake() { try { + const particle = this const script = (this.content === undefined ? "" : this.content) + "\n" + this.subparticlesToString() const evaled = eval(script) - if (evaled) - this.replaceParticle(() => evaled.toString()) + await this.root.appendFromStream(evaled) } catch (err) { console.error(err) } diff --git a/parsers/macros.parsers b/parsers/macros.parsers index cda2edb49..500c25828 100644 --- a/parsers/macros.parsers +++ b/parsers/macros.parsers @@ -1,7 +1,7 @@ abstractMacroParser extends abstractScrollParser catchAllAtomType stringAtom - atoms preBuildCommandAtom + atoms parseTimeCommandAtom cueFromId javascript get search() { @@ -27,7 +27,7 @@ abstractMacroParser replaceParser description Replace this with that. extends abstractMacroParser - atoms preBuildCommandAtom codeAtom + atoms parseTimeCommandAtom codeAtom baseParser blobParser example replace YEAR 2022 @@ -42,6 +42,7 @@ replaceJsParser javascript get replacement() { const particle = this + const codeAtStart = this.root.codeAtStart || "" try { return eval(super.replacement) } catch (err) { @@ -49,20 +50,3 @@ replaceJsParser } return this.search } - -toFooterParser - extends abstractScrollParser - description Experimental way to move a section to the footer. - atoms preBuildCommandAtom - cueFromId - javascript - buildHtml() { - // todo: very hacky! fix. - this.section.forEach(particle => { - const clone = particle.clone() - this.root.appendParticle(clone) - particle.destroy() - }) - this.destroy() - return "" - } diff --git a/parsers/moves.parsers b/parsers/moves.parsers new file mode 100644 index 000000000..1d6966ad4 --- /dev/null +++ b/parsers/moves.parsers @@ -0,0 +1,18 @@ +moveToFooterParser + extends abstractScrollParser + description Move a section to the footer. + atoms parseTimeCommandAtom + cueFromId + javascript + async wake() { + this.root.notifyOnReady(this) + } + onReady() { + this.section.forEach(particle => { + const clone = particle.clone() + this.root.appendParticle(clone) + particle.destroy() + }) + this.destroy() + return "" + } diff --git a/parsers/nodejs.parsers b/parsers/nodejs.parsers index 93957f521..2e14e062b 100644 --- a/parsers/nodejs.parsers +++ b/parsers/nodejs.parsers @@ -21,7 +21,7 @@ evalNodejsParser extends abstractScrollParser catchAllAtomType javascriptAnyAtom baseParser blobParser - atoms preBuildCommandAtom + atoms parseTimeCommandAtom cueFromId example evalNodejs @@ -30,5 +30,5 @@ evalNodejsParser wake() { const evaled = this.root.evalNodeJs(this) if (evaled !== false) - this.replaceParticle(() => evaled) + this.replaceWith(evaled) } \ No newline at end of file diff --git a/parsers/parsers.parsers b/parsers/parsers.parsers index c003cd793..b36be6065 100644 --- a/parsers/parsers.parsers +++ b/parsers/parsers.parsers @@ -122,7 +122,7 @@ stringAtom paint string atomAtom - paint constant + paint string description A non-empty single atom string. regex .+ @@ -452,6 +452,11 @@ atomTypeDefinitionParser inScope parsersPaintParser parsersRegexParser reservedAtomsParser enumFromAtomTypesParser atomTypeDescriptionParser parsersEnumParser slashCommentParser extendsAtomTypeParser parsersExamplesParser atomMinParser atomMaxParser atoms atomTypeIdAtom tags assemblePhase + javascript + buildHtml() {return ""} + async wake() { + this.root.stackParserCode(this) + } // Enums enumFromAtomTypesParser @@ -504,6 +509,11 @@ parserDefinitionParser inScope abstractParserRuleParser abstractConstantParser slashCommentParser parserDefinitionParser atoms parserIdAtom tags assemblePhase + javascript + buildHtml() { return ""} + async wake() { + this.root.stackParserCode(this) + } parsersRegexParser catchAllAtomType regexAtom diff --git a/parsers/posts.parsers b/parsers/posts.parsers index 3af190c33..829bbbe55 100644 --- a/parsers/posts.parsers +++ b/parsers/posts.parsers @@ -10,7 +10,7 @@ abstractPostsParser const {fileSystem} = this.root for (let folderPath of dependsOn) { // console.log(`${this.root.filePath} is loading: ${folderPath} in id '${fileSystem.scrollFileSystemIdNumber}'`) - await fileSystem.getLoadedFilesInFolder(folderPath, ".scroll") + await fileSystem.getFusedFilesInFolder(folderPath, ".scroll") } } get tags() { diff --git a/parsers/root.parsers b/parsers/root.parsers index 09bc1ee6d..954789b7e 100644 --- a/parsers/root.parsers +++ b/parsers/root.parsers @@ -54,8 +54,6 @@ scrollParser javascript 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) { @@ -107,8 +105,14 @@ scrollParser const { scrollProgram } = file return scrollProgram.buildsHtml && scrollProgram.hasKeyboardNav && scrollProgram.tags.includes(this.primaryTag) } - get timeIndex() { - return this.file.timeIndex || 0 + _timestamp + get timestamp() { + if (!this._timestamp) { + this._timestamp = this.file.timestamp + const date = this.get("date") + if (date) this._timestamp = this.dayjs(date).unix() + } + return this._timestamp } get linkToPrevious() { if (!this.hasKeyboardNav) @@ -118,7 +122,7 @@ scrollParser let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).previous if (!file) return undefined while (!this._includeFileInKeyboardNav(file)) { - file = this._nextAndPrevious(allScrollFiles, file.timeIndex).previous + file = this._nextAndPrevious(allScrollFiles, file.scrollProgram.timeIndex).previous } return file.scrollProgram.permalink } @@ -130,7 +134,7 @@ scrollParser let file = this._nextAndPrevious(allScrollFiles, this.timeIndex).next if (!file) return undefined while (!this._includeFileInKeyboardNav(file)) { - file = this._nextAndPrevious(allScrollFiles, file.timeIndex).next + file = this._nextAndPrevious(allScrollFiles, file.scrollProgram.timeIndex).next } return file.scrollProgram.permalink } @@ -174,7 +178,7 @@ scrollParser const {folderPath, group, relativePath} = this.parseNestedTag(tag) let files = [] try { - files = this.fileSystem.getCachedLoadedFilesInFolder(folderPath, this) + files = this.fileSystem.getFusedFilesInFolderIfCached(folderPath, this) } catch (err) { console.error(err) } @@ -183,7 +187,7 @@ scrollParser }) arr = arr.concat(filtered.slice(0, limit)) }) - return this.lodash.sortBy(arr, file => file.file.timestamp).reverse() + return this.lodash.sortBy(arr, file => file.file.scrollProgram.timestamp).reverse() } async fetchNode(url, filename) { filename = filename || new URL(url).pathname.split('/').pop() @@ -266,9 +270,19 @@ scrollParser get authors() { return this.get("authors") } + _addTimeIndexes(files) { + // A little hacky but ah well. + // For blogs, we want to sort posts in a folder in order of the date property. + // If the date property is not set, we use the file timestamp from disk. + const sorted = files.sort((a, b) => b.scrollProgram.timestamp - a.scrollProgram.timestamp) + sorted.forEach((file, index) => (file.scrollProgram.timeIndex = index)) + } get allScrollFiles() { try { - return this.fileSystem.getCachedLoadedFilesInFolder(this.folderPath, this) + const files = this.fileSystem.getFusedFilesInFolderIfCached(this.folderPath, this) + if (this.timeIndex === undefined) + this._addTimeIndexes(files) + return files } catch (err) { console.error(err) return [] @@ -287,6 +301,17 @@ scrollParser getFromParserId(parserId) { return this.parserIdIndex[parserId]?.[0].content } + notifyOnReady(particle) { + if (!this._toNotify) + this._toNotify = [] + this._toNotify.push(particle) + } + wake() { + // In case the file ended on a parser definition, refresh parser pool + this._refreshParserPool() + if (this._toNotify) + this._toNotify.forEach(particle => particle.onReady()) + } get fileSystem() { return this.file.fileSystem } @@ -367,7 +392,7 @@ scrollParser } get scrollVersion() { // currently manually updated - return "176.0.1" + return "177.0.0" } // Use the first paragraph for the description // todo: add a particle method version of get that gets you the first particle. (actulaly make get return array?) @@ -432,9 +457,12 @@ scrollParser const files = this.topDownArray.filter(particle => particle.dependencies).map(particle => particle.dependencies).flat() return dependencies.concat(files) } + get importOnly() { + return this.has("importOnly") + } get buildsHtml() { const { permalink } = this - return !this.file.importOnly && (permalink.endsWith(".html") || permalink.endsWith(".htm")) + return !this.importOnly && (permalink.endsWith(".html") || permalink.endsWith(".htm")) } // Without specifying the language hyphenation will not work. get lang() { @@ -463,7 +491,7 @@ scrollParser return this.dateObject.format(`MM/DD/YYYY`) } get dateObject() { - const date = this.get("date") || (this.file.timestamp ? this.file.timestamp : 0) + const date = this.get("date") || (this.timestamp || 0) return this.dayjs(date) } get year() { @@ -515,11 +543,11 @@ scrollParser }) .join("\n") concept.forEach((particle, index) => (index ? particle.destroy() : "")) - concept[0].replaceParticle(() => newCode) + concept[0].replaceWith(newCode) }) } get formatted() { - return this.getFormatted(this.file.codeAtStart) + return this.getFormatted(this.codeAtStart) } get lastCommitTime() { // todo: speed this up and do a proper release. also could add more metrics like this. @@ -532,9 +560,17 @@ scrollParser } return this._lastCommitTime } + get codeAtStart() { + return this.file.codeAtStart + } + async ensureFileLoaded() { + // todo: cleanup + if (!this.codeAtStart) + await this.file._readCodeFromStorage() + } getFormatted(codeAtStart = this.toString()) { let formatted = codeAtStart.replace(/\r/g, "") // remove all carriage returns if there are any - const parsed = new this.constructor(formatted) + const parsed = new this.latestConstructor(formatted) parsed.topDownArray.forEach(subparticle => { subparticle.format() const original = subparticle.getLine() @@ -548,10 +584,16 @@ scrollParser let allElse = [] // Create any bindings parsed.forEach(particle => { - if (particle.bindTo === "next") particle.binding = particle.next - if (particle.bindTo === "previous") particle.binding = particle.previous + if (particle.bindTo === "next" && !particle.isLast) particle.binding = particle.next + if (particle.bindTo === "previous" && !particle.isFirst) particle.binding = particle.previous }) + let noFormat = false parsed.forEach(particle => { + if (noFormat) return allElse.push(particle) + if (particle.getLine() === "noFormat") { + noFormat = true + return allElse.push(particle) + } if (particle.getLine() === "importOnly") importOnlys.push(particle) else if (particle.isTopMatter) topMatter.push(particle) else allElse.push(particle) @@ -559,7 +601,7 @@ scrollParser const combined = importOnlys.concat(topMatter, allElse) // Move any bound particles combined - .filter(particle => particle.bindTo) + .filter(particle => particle.binding) .forEach(particle => { // First remove the particle from its current position const originalIndex = combined.indexOf(particle) @@ -606,6 +648,30 @@ scrollParser await particle.buildTwo(options) } } + stackParserCode(parser) { + // Add top level parsers only which will add their nested parsers. + if (parser.parent !== this) return + // Perf hack until we can refactor ParserPool and add incremental addition of parsers. + // What we see in the wild is that frequently you'll have parser definitions one after + // the other, separated by blank lines or comments. So we stack up those parsers and + // dont add them until we are done with that section. + if (this._parsersToRegister === undefined) + this._parsersToRegister = [] + this._parsersToRegister.push(parser) + if (!this._beforeAppend) + this._beforeAppend = (block) => { + const line1 = block.split(/\n/)[0] + if (line1 === "" || line1.endsWith("Parser") || line1.startsWith("//")) + return + this._refreshParserPool() + } + } + _refreshParserPool() { + if (!this._parsersToRegister) return + this.registerParsers(this._parsersToRegister.map(p => p.toString()).join("\n"), this.filePath) + delete this._beforeAppend + delete this._parsersToRegister + } get outputFileNames() { return this.filter(p => p.outputFileNames).map(p => p.outputFileNames).flat() } @@ -690,26 +756,20 @@ scrollParser 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 + get latestConstructor() { + // todo: cleanup + return this._modifiedConstructor || this.constructor + } + _parseMeasures() { + const {lodash, definition} = this + // cache measures on the _Parser_, since they are unique to a parser. + if (definition._measures) + return definition._measures // 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"), this.getLine() - ) + const dummyCode = Array.from(new Set(definition.map(particle => particle.get("cue") || particle.getLine().replace(/Parser$/, "")))).join("\n") + const parser = this.latestConstructor + const dummyProgram = new parser(dummyCode, this.getLine()) // Delete any particles that are not measures dummyProgram.filter(particle => !particle.isMeasure).forEach(particle => particle.destroy()) dummyProgram.forEach(particle => { @@ -735,22 +795,25 @@ scrollParser Cue: particle.definition.get("cue") } }) - measureCache.set(parser, lodash.sortBy(measures, "SortIndex")) - return measureCache.get(parser) + definition._measures = lodash.sortBy(measures, "SortIndex") + return definition._measures } _concepts get concepts() { - if (this._concepts) return this._concepts - this._concepts = this.parseConcepts(this, this.measures) + if (!this._concepts) this._concepts = this._parseConcepts() return this._concepts } + get concept() { + return this.concepts[0] + } _measures get measures() { - if (this._measures) return this._measures - this._measures = this.parseMeasures(this.parser) + if (!this._measures) return this._measures = this._parseMeasures() return this._measures } - parseConcepts(parsedProgram, measures){ + _parseConcepts(){ + const parsedProgram = this + const measures = this.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 diff --git a/parsers/source.parsers b/parsers/source.parsers index 2cea8e45b..d5d76ef6b 100644 --- a/parsers/source.parsers +++ b/parsers/source.parsers @@ -8,5 +8,5 @@ printSourceParser javascript buildHtml() { const files = this.root.getFilesByTags(this.content).map(file => file.file) - return `${files.map(file => file.scrollProgram.filePath + "\n " + file.codeAtStart.replace(/\n/g, "\n ") ).join("\n")}` + return `${files.map(file => file.scrollProgram).map(scrollProgram => scrollProgram.filePath + "\n " + scrollProgram.codeAtStart.replace(/\n/g, "\n ") ).join("\n")}` } diff --git a/parsers/sourcemap.parsers b/parsers/sourcemap.parsers index 322aed046..dac2828a2 100644 --- a/parsers/sourcemap.parsers +++ b/parsers/sourcemap.parsers @@ -17,7 +17,7 @@ debugSourceMapParser currentFile.lineNumber++ currentFile.linesLeft-- if (particle.cue === "imported") { - const linesLeft = parseInt(particle.get("lines")) + const linesLeft = parseInt(particle.get("particles")) const original = particle.get("original") fileStack.push({ fileName: particle.atoms[1], lineNumber: 0, linesLeft }) return `${currentFile.fileName}:${currentFile.lineNumber} ${original}\n` + particle.map(line => `${currentFile.fileName}:${currentFile.lineNumber} ${line}`).join("\n") diff --git a/parsers/stamp.parsers b/parsers/stamp.parsers index 228eaedec..edfd230bf 100644 --- a/parsers/stamp.parsers +++ b/parsers/stamp.parsers @@ -143,7 +143,7 @@ stampParser hello.js console.log("Hello world") cueFromId - atoms preBuildCommandAtom + atoms parseTimeCommandAtom javascript execute() { const dir = this.root.folderPath diff --git a/releaseNotes.scroll b/releaseNotes.scroll index 5b4eb8731..b12c2c541 100644 --- a/releaseNotes.scroll +++ b/releaseNotes.scroll @@ -22,6 +22,18 @@ ciBadges.scroll br thinColumns +📦 177.0.0 4/06/2025 +🎉 Scroll is now a single-pass language! +🎉 added assertBuildEndsWithParser +🎉 added conceptTemplateParser +🎉 added dumpConceptParser +🎉 added noFormatParser +🏥 update ScrollSDK to 107 +🏥 fixed regression in concept building +🏥 fixed bug in custom dinkus +⚠️ BREAKING: `toFooter` is now `moveToFooter` +⚠️ BREAKING: `footer` in an import block is now `moveToFooter` + 📦 176.0.1 3/29/2025 🏥 update ScrollSDK @@ -91,6 +103,7 @@ This text should be aligned right. 🎉 Added `noSnippet` parser that can be used in all aftertext parsers to prevent HTML generation of that particle when generating a snippet. 🏥 fix bug with date printing in short snippets. ⚠️ BREAKING: removed stump parsers. if you were using `stump` parsers, just unnest subparticles under stump and delete stump atom. +⚠️ BREAKING: remove `stumpNoSnippet` codeWithHeader Before: stump diff --git a/scroll.js b/scroll.js index 89c173369..aa1412123 100755 --- a/scroll.js +++ b/scroll.js @@ -60,10 +60,9 @@ footer.scroll` } async scrollToHtml(scrollCode) { - const ScrollFile = this.sfs.defaultFileClass - const page = new ScrollFile(scrollCode) - await page.fuse() - return page.scrollProgram.asHtml + const file = this.sfs.newFile(scrollCode) + await file.singlePassFuse() + return file.scrollProgram.asHtml } sfs = new ScrollFileSystem(undefined, path.join(__dirname, "parsers")) @@ -80,18 +79,12 @@ footer.scroll` async getErrorsInFolder(folder) { const fileSystem = this.sfs const folderPath = ensureFolderEndsInSlash(folder) - const files = await fileSystem.getLoadedFilesInFolder(folderPath, ".scroll") // Init/cache all parsers - - // todo: cleanup - const parsers = await Promise.all(Object.values(fileSystem._parserCache)) - const parserErrors = parsers.map(parser => parser.parsersParser.getAllErrors().map(err => err.toObject())).flat() - - const scrollErrors = await this.getErrorsInFiles(files) - return { parserErrors, scrollErrors } + const files = await fileSystem.getFusedFilesInFolder(folderPath, ".scroll") // Init/cache all parsers + return await this.getErrorsInFiles(files) } async getErrorsInFiles(files) { - // todo: what about parser errors? + // todo: re-add parser errors for (let file of files) await file.scrollProgram.load() return files .map(file => { @@ -107,49 +100,42 @@ footer.scroll` const start = Date.now() const folder = this.resolvePath(cwd) let target = cwd - let parserErrors = [] let scrollErrors = [] if (filenames && filenames.length) { const files = await this.getFiles(cwd, filenames) scrollErrors = await this.getErrorsInFiles(files) target = filenames.join(" ") } else { - const results = await this.getErrorsInFolder(folder) - parserErrors = results.parserErrors - scrollErrors = results.scrollErrors + scrollErrors = await this.getErrorsInFolder(folder) } const seconds = (Date.now() - start) / 1000 - if (parserErrors.length) { - this.log(``) - this.log(`❌ ${parserErrors.length} parser errors in "${cwd}"`) - this.log(new Particle(parserErrors).toFormattedTable(200)) - this.log(``) - } if (scrollErrors.length) { this.log(``) this.log(`❌ ${scrollErrors.length} errors in "${cwd}"`) this.log(new Particle(scrollErrors).toFormattedTable(100)) this.log(``) } - if (!parserErrors.length && !scrollErrors.length) return this.log(`✅ 0 errors in "${target}". Tests took ${seconds} seconds.`) - return `${parserErrors.length + scrollErrors.length} Errors` + if (!scrollErrors.length) return this.log(`✅ 0 errors in "${target}". Tests took ${seconds} seconds.`) + return `${scrollErrors.length} Errors` } async formatCommand(cwd, filenames) { let files = [] if (filenames && filenames.length) files = await this.getFiles(cwd, filenames) - else files = await this.sfs.getLoadedFilesInFolder(this.resolvePath(cwd), ".scroll") - // .concat(fileSystem.getLoadedFilesInFolder(folder, ".parsers")) // todo: should format parser files too. + else files = await this.sfs.getFusedFilesInFolder(this.resolvePath(cwd), ".scroll") + // .concat(fileSystem.getFusedFilesInFolder(folder, ".parsers")) // todo: should format parser files too. for (let file of files) { - this.formatFile(file) + this.formatFile(file.scrollProgram) } } - async formatFile(file) { - const { formatted, filePath, filename } = file.scrollProgram - const { codeAtStart } = file + // Formatting is currently defined as formatting the entire original source file + // using the last parser present. + async formatFile(scrollProgram) { + await scrollProgram.ensureFileLoaded() + const { formatted, filePath, filename, codeAtStart } = scrollProgram if (codeAtStart === formatted) return await this.sfs.write(filePath, formatted) this.log(`💾 formatted ${filename}`) @@ -165,7 +151,7 @@ footer.scroll` async getFiles(cwd, filenames) { const fullPaths = this.resolveFilenames(cwd, filenames) - const files = await Promise.all(fullPaths.map(fp => this.sfs.getLoadedFile(fp))) + const files = await Promise.all(fullPaths.map(fp => this.sfs.getFusedFile(fp))) return files } @@ -183,7 +169,7 @@ footer.scroll` const start = Date.now() // Run the build loop twice. The first time we build ScrollSets, in case some of the HTML files // will depend on csv/tsv/json/etc - const toBuild = files.filter(file => !file.importOnly) + const toBuild = files.filter(file => !file.scrollProgram.importOnly) options.externalFilesCopied = {} for (const file of toBuild) { file.scrollProgram.logger = this @@ -210,7 +196,7 @@ footer.scroll` async buildFilesInFolder(folder = "/", fileSystem = this.sfs) { folder = ensureFolderEndsInSlash(folder) - const files = await fileSystem.getLoadedFilesInFolder(folder, ".scroll") + const files = await fileSystem.getFusedFilesInFolder(folder, ".scroll") this.log(`Found ${files.length} scroll files in '${folder}'\n`) return await this.buildFiles(fileSystem, files, folder) } diff --git a/tests/a-review-of-my-sink.scroll b/tests/a-review-of-my-sink.scroll index 032a63a50..30ddb1d76 100644 --- a/tests/a-review-of-my-sink.scroll +++ b/tests/a-review-of-my-sink.scroll @@ -1,10 +1,10 @@ +tags index all +title A review of my sink replace SOME_DATA - foo link bar.html - bar link bam.html -tags index all -title A review of my sink header.scroll diff --git a/tests/aParser.scroll b/tests/aParser.scroll index 11dfd27ea..ec5e40bee 100644 --- a/tests/aParser.scroll +++ b/tests/aParser.scroll @@ -1,3 +1,10 @@ +buildHtml + +The below should just be parsed as text: +hiddenMessage Click me. + message Hello world +assertHtmlExcludes onclick="ale + hiddenMessageParser extends scrollParagraphParser inScope messageParser @@ -6,5 +13,8 @@ hiddenMessageParser buildHtml() { return `${super.buildHtml()}` } + +The below should be parsed by the parser above hiddenMessage Click me. message Hello world +assertHtmlIncludes onclick="ale diff --git a/tests/about.scroll b/tests/about.scroll index 2de2bed2c..9dd1816dc 100644 --- a/tests/about.scroll +++ b/tests/about.scroll @@ -1,6 +1,6 @@ -replace KEY_MESSAGE Public domain products are strictly superior to equivalent non-public domain alternatives by a significant margin on three dimensions: trust, speed, and cost to build. tags all title About the Kitchen Sink Blog +replace KEY_MESSAGE Public domain products are strictly superior to equivalent non-public domain alternatives by a significant margin on three dimensions: trust, speed, and cost to build. header.scroll diff --git a/tests/atomTypes.scroll b/tests/atomTypes.scroll new file mode 100644 index 000000000..0b388ad27 --- /dev/null +++ b/tests/atomTypes.scroll @@ -0,0 +1,23 @@ +buildHtml + +streetLightAtom + extends stringAtom + enum red green yellow + paint constant + +lightParser + cueFromId + extends abstractScrollParser + atoms cueAtom streetLightAtom + javascript + buildHtml() { + return `[0]` + } + +light red +assertHtmlIncludes color:red + +// Test overwriting an atom. The original atomType should still be used above. +streetLightAtom + extends stringAtom + enum orange purple diff --git a/tests/authors.scroll b/tests/authors.scroll index 46a1e0e9f..5c88ec25b 100644 --- a/tests/authors.scroll +++ b/tests/authors.scroll @@ -1,4 +1,4 @@ -Authors should not print anything: authors Breck Yunits https://breckyunits.com assertHtmlExcludes Breck +Authors should not print anything. diff --git a/tests/concepts.scroll b/tests/concepts.scroll index 69453e10b..bd5712bfc 100644 --- a/tests/concepts.scroll +++ b/tests/concepts.scroll @@ -18,3 +18,7 @@ notes Daughter - Samantha. phone +1 (555) 123-4562 email jill@gmail.com birthday 1/23/80 + +conceptTemplate + # This is a page for {name} + His phone is {phone} diff --git a/tests/contacts.scroll b/tests/contacts.scroll index f97be0bc0..367a0c725 100644 --- a/tests/contacts.scroll +++ b/tests/contacts.scroll @@ -1,11 +1,13 @@ buildConcepts contacts.csv contacts.json contacts.tsv buildHtml title My Contacts - -../microlangs/contacts.parsers +container theme gazette +../microlangs/contacts.parsers + mediumColumns 1 printTitle +// This is a little confusing. This datatable is fed by the csv's generated above (I think.). Datatable looks at permalink name for a matching csv. datatable printTable assertRowCount 2 diff --git a/tests/debugging.scroll b/tests/debugging.scroll index 04a8f5cc0..6902dfd2b 100644 --- a/tests/debugging.scroll +++ b/tests/debugging.scroll @@ -1,6 +1,7 @@ buildHtml replace TITLE Debug tools tests replaceJs SUM 1+1 +noFormat title TITLE theme gazette diff --git a/tests/dumpParsers.scroll b/tests/dumpParsers.scroll new file mode 100644 index 000000000..e7f33a3a0 --- /dev/null +++ b/tests/dumpParsers.scroll @@ -0,0 +1,7 @@ +buildParsers dumpParsers.txt +assertBuildIncludes Carpe diem! +// Just test to ensure some source code from a parser is included + +// This prints all parsers. +// We use it to dump all parsers to a single file to power try.scroll.pub +debugParsers diff --git a/tests/html.scroll b/tests/html.scroll index 0b0f6797c..31d46c8b6 100644 --- a/tests/html.scroll +++ b/tests/html.scroll @@ -14,7 +14,6 @@ meta content tag assertHtmlIncludes /> - ol li A list item assertHtmlIncludes diff --git a/tests/json.scroll b/tests/json.scroll index dc15f01cf..4ef6bb98f 100644 --- a/tests/json.scroll +++ b/tests/json.scroll @@ -11,4 +11,4 @@ script ../package.json path prettier - printTable \ No newline at end of file + printTable diff --git a/tests/modal.scroll b/tests/modal.scroll index 1adb2ce16..6e8cba34a 100644 --- a/tests/modal.scroll +++ b/tests/modal.scroll @@ -9,7 +9,6 @@ modal id m1 # This should be in a modal - Open modal 2. link #m2 modal @@ -19,4 +18,3 @@ modal id m2 # This is modal 2. loremIpsum 2 - diff --git a/tests/push.scroll b/tests/moveToFooter.scroll similarity index 66% rename from tests/push.scroll rename to tests/moveToFooter.scroll index 72967eb72..e4c0a98ef 100644 --- a/tests/push.scroll +++ b/tests/moveToFooter.scroll @@ -1,5 +1,8 @@ buildHtml -toFooter -This is my footer. It is moved to end of file before compilation. +buildTxt +assertBuildEndsWith The End. + +moveToFooter +This is my footer. It is moved to end of file before compilation. The End. This is my body. This should be above the footer. diff --git a/tests/parsersImports.scroll b/tests/parsersImports.scroll new file mode 100644 index 000000000..457816670 --- /dev/null +++ b/tests/parsersImports.scroll @@ -0,0 +1,11 @@ +buildHtml + +extension.parsers + +// This should not trigger an uncaught error +assertSilenceBelowErrors +fileDoesNotExist.parsers + +goodMorning +assertHtmlIncludes Good morning + diff --git a/tests/postTemplate.scroll b/tests/postTemplate.scroll index 4bc30ca09..053469c68 100644 --- a/tests/postTemplate.scroll +++ b/tests/postTemplate.scroll @@ -2,7 +2,7 @@ importOnly buildHtml header.scroll footer.scroll - footer + moveToFooter printTitle diff --git a/tests/scroll.test.js b/tests/scroll.test.js index 321eddf65..65be3a422 100755 --- a/tests/scroll.test.js +++ b/tests/scroll.test.js @@ -3,6 +3,7 @@ const tap = require("tap") const fs = require("fs") const path = require("path") +const { Particle } = require("scrollsdk/products/Particle.js") const { ScrollCli } = require("../scroll.js") const { ScrollSetCLI } = require("../ScrollSetCLI.js") const { Disk } = require("scrollsdk/products/Disk.node.js") @@ -15,7 +16,6 @@ const testParticles = {} const testsFolder = path.join(__dirname) const stampFolder = path.join(testsFolder, "testOutput") const cli = new ScrollCli() -const ScrollFile = cli.sfs.defaultFileClass // cleanup in case it was built earlier: if (Disk.exists(stampFolder)) fs.rmSync(stampFolder, { recursive: true }) @@ -124,7 +124,7 @@ buildHtml` testParticles.file = async areEqual => { const rootFolder = path.join(__dirname, "..") const cli = new ScrollCli().silence() - const files = await cli.sfs.getLoadedFilesInFolder(rootFolder, ".scroll") + const files = await cli.sfs.getFusedFilesInFolder(rootFolder, ".scroll") const releaseNotesFile = files.find(file => file.scrollProgram.permalink === "releaseNotes.html").scrollProgram areEqual(releaseNotesFile.permalink, "releaseNotes.html") @@ -134,7 +134,7 @@ testParticles.file = async areEqual => { } testParticles.ensureNoErrorsInParser = async areEqual => { - const DefaultScrollParser = cli.sfs.defaultParser.parser + const DefaultScrollParser = cli.sfs.defaultParser const parserErrors = new parsersParser(new DefaultScrollParser().definition.asString).getAllErrors().map(err => err.toObject()) if (parserErrors.length) console.log(parserErrors) areEqual(parserErrors.length, 0, "no errors in scroll standard library parsers") @@ -165,12 +165,12 @@ testParticles.cli = async areEqual => { testParticles.standalonePage = async areEqual => { // Arrange - const page = new ScrollFile(`title A standalone page + const file = cli.sfs.newFile(`title A standalone page printTitle * Blue sky`) // Act/Assert - await page.fuse() - const { asHtml, asTxt } = page.scrollProgram + await file.singlePassFuse() + const { asHtml, asTxt } = file.scrollProgram areEqual(asHtml.includes("Blue sky"), true) areEqual(asTxt.includes("A standalone page"), true) } @@ -213,33 +213,33 @@ testParticles.scrollsetCli = areEqual => { testParticles.format = async areEqual => { // Arrange - const page = new ScrollFile(``) + const file = cli.sfs.newFile("") // Act/Assert - await page.fuse() - areEqual(page.scrollProgram.formatted, "", "format works") + await file.singlePassFuse() + areEqual(file.scrollProgram.formatted, "", "format works") - const page2 = new ScrollFile(`# hi`) + const file2 = cli.sfs.newFile(`# hi`) // Act/Assert - await page2.fuse() - areEqual(page2.scrollProgram.formatted, "# hi\n", "format works") + await file2.singlePassFuse() + areEqual(file2.scrollProgram.formatted, "# hi\n", "format works") } testParticles.outputFileNames = async areEqual => { // Arrange - const page = new ScrollFile(``) + const file = cli.sfs.newFile("") // Act - await page.fuse() + await file.singlePassFuse() // Assert - areEqual(page.scrollProgram.outputFileNames.length, 0, "no outputFileNames in a blank file") + areEqual(file.scrollProgram.outputFileNames.length, 0, "no outputFileNames in a blank file") // Arrange - const page2 = new ScrollFile(`buildHtml foo.html + const file2 = cli.sfs.newFile(`buildHtml foo.html buildTxt foo.txt `) // Act - await page2.fuse() + await file2.singlePassFuse() // Assert - areEqual(page2.scrollProgram.outputFileNames[1], "foo.txt") + areEqual(file2.scrollProgram.outputFileNames[1], "foo.txt") } testParticles.initCommand = async areEqual => { @@ -257,13 +257,13 @@ testParticles.initCommand = async areEqual => { const products = await cli.buildFilesInFolder(tempFolder) // Assert - areEqual(Disk.read(products[3]).includes("Built with Scroll"), true, "has message") + const helloWorldPath = path.join(tempFolder, "helloWorld.html") + areEqual(Disk.read(helloWorldPath).includes("Built with Scroll"), true, "has message") areEqual(products.filter(name => name.endsWith(".html")).length, 2, "should have 2 html pages") areEqual(products.length, 7, "should have 7 total generated files") - const { scrollErrors, parserErrors } = await cli.getErrorsInFolder(tempFolder) + const scrollErrors = await cli.getErrorsInFolder(tempFolder) areEqual(scrollErrors.length, 0) - areEqual(parserErrors.length, 0) } catch (err) { console.log(err) } @@ -299,6 +299,133 @@ testParticles.hodgePodge = async areEqual => { fs.rmSync(stampFolder, { recursive: true }) } +const stripImported = str => { + const particle = new Particle(str) + particle.getParticles("imported").forEach(particle => particle.destroy()) + return particle.toString() +} + +testParticles.inMemory = async equal => { + // Arrange/Act/Assert + const cli = new ScrollCli().silence() + const files = { + "/hello.scroll": "world", + "/main.scroll": "import hello.scroll\nimport nested/test.scroll", + "/nested/test.scroll": "ciao", + "/nested/deep/relative.scroll": "import ../../hello.scroll\nimport ../test.scroll" + } + cli.initFs(files) + const fused = await cli.sfs.getFusedFilesInFolder("/", "scroll") + const mainResult = fused.find(file => file.filePath === "/main.scroll") + equal(stripImported(mainResult.scrollProgram), "world\nciao") + equal(mainResult.scrollProgram.toString().includes("exists true"), true) + + const relativeResult = await cli.sfs.getFusedFile("/nested/deep/relative.scroll") + equal(stripImported(relativeResult.scrollProgram), "world\nciao") +} + +testParticles.empty = async equal => { + // Arrange + const cli = new ScrollCli().silence() + const files = { + "/hello.scroll": "", + "/main.scroll": "import hello.scroll\nhi" + } + cli.initFs(files) + + // Act + const fused = await cli.sfs.getFusedFilesInFolder("/", "scroll") + const mainResult = await cli.sfs.getFusedFile("/main.scroll") + // Assert + equal(stripImported(mainResult.scrollProgram), `\nhi`) +} + +testParticles.nonExistant = async equal => { + // Arrange/Act/Assert + const cli = new ScrollCli().silence() + const files = { + "/main.scroll": "import env.scroll" + } + cli.initFs(files) + const fused = await cli.sfs.getFusedFilesInFolder("/", "scroll") + const result = await cli.sfs.getFusedFile("/main.scroll") + equal(stripImported(result.scrollProgram.toString()), "") + equal(result.scrollProgram.toString().includes("exists false"), true) +} + +testParticles.footers = async equal => { + // Arrange/Act/Assert + const cli = new ScrollCli().silence() + const files = { + "/hello.scroll": `headerAndFooter.scroll +title Hello world +This is my content +`, + "/headerAndFooter.scroll": `header.scroll +footer.scroll + moveToFooter`, + "/header.scroll": "printTitle", + "/footer.scroll": "The end." + } + cli.initFs(files) + await cli.sfs.getFusedFilesInFolder("/", "scroll") + const result = await cli.sfs.getFusedFile("/hello.scroll") + equal(result.scrollProgram.toString().includes("This is my content"), true) + equal(result.scrollProgram.toString().endsWith("The end."), true, "ends with footer") +} + +testParticles.circularImports = async equal => { + const cli = new ScrollCli().silence() + const files = { + "/a.scroll": "b.scroll", + "/b.scroll": "a.scroll", + "/c.scroll": "c.scroll", + "/d.scroll": "e.scroll\nf.scroll", + "/e.scroll": "f.scroll", + "/f.scroll": "g.scroll", + "/g.scroll": "" + } + cli.initFs(files) + const sfs = cli.sfs + await sfs.getFusedFilesInFolder("/", "scroll") + const result2 = await sfs.getFusedFile("/c.scroll") + equal(result2.scrollProgram.toString().includes("circularImportError"), true, "Should have detected circularImports") + const result = await sfs.getFusedFile("/a.scroll") + equal(result.scrollProgram.toString().includes("circularImportError"), true, "Should have detected circularImports") + const result3 = await sfs.getFusedFile("/d.scroll") + equal(result3.scrollProgram.toString().includes("circularImportError"), false, "No circularImports detected") +} + +testParticles.quickImports = async equal => { + // Arrange/Act/Assert + const cli = new ScrollCli().silence() + const files = { + "/hello.scroll": "world", + "/main": "hello.scroll\nnested/test.scroll", + "/nested/test.scroll": "ciao", + "/nested/a": "test.scroll", + "/nested/deep/relative": "../../hello.scroll\n../test.scroll" + } + cli.initFs(files) + const sfs = cli.sfs + equal(sfs.dirname("/"), "/") + + const [aResult, mainResult, relativeResult] = await Promise.all([sfs.getFusedFile("/nested/a"), sfs.getFusedFile("/main"), sfs.getFusedFile("/nested/deep/relative")]) + equal(stripImported(aResult.scrollProgram.toString()), "ciao") + equal(stripImported(mainResult.scrollProgram.toString()), "world\nciao") + equal(stripImported(relativeResult.scrollProgram.toString()), "world\nciao") + + // FileAPI + // Arrange + const file = sfs.newFile(files["/main"], "/main") + equal(file.scrollProgram.toString(), "") + // Act + // TODO: make this singlePassFuse + await file.singlePassFuse() + // Assert + equal(stripImported(file.scrollProgram.toString()), "world\nciao") +} + if (module && !module.parent) TestRacer.testSingleFile(__filename, testParticles) module.exports = { testParticles } diff --git a/tests/sourcemaps.scroll b/tests/sourcemaps.scroll index 39a7c4e58..ff2dd78c0 100644 --- a/tests/sourcemaps.scroll +++ b/tests/sourcemaps.scroll @@ -8,6 +8,6 @@ header.scroll footer.scroll ## Now print source map: +// Test this using assertHtmlMatches rather than assertHtmlIncludes because a simple string search of the source would just match the string in the source. debugSourceMap assertHtmlMatches settings.scroll:\d extension.parsers -// Test this using assertHtmlMatches rather than assertHtmlIncludes because a simple string search of the source would just match the string in the source. \ No newline at end of file diff --git a/tests/textAlign.scroll b/tests/textAlign.scroll index aca16d6ba..1cbe5ee9e 100644 --- a/tests/textAlign.scroll +++ b/tests/textAlign.scroll @@ -23,4 +23,4 @@ assertHtmlIncludes center > Veni. Vidi. Vici. Julius caesar right -assertHtmlIncludes text-align: right; \ No newline at end of file +assertHtmlIncludes text-align: right; diff --git a/tests/top-sinks.scroll b/tests/top-sinks.scroll index b8dcb6e7c..8640d4837 100644 --- a/tests/top-sinks.scroll +++ b/tests/top-sinks.scroll @@ -2,6 +2,8 @@ authors Breck Yunits https://breckyunits.com Breck Yunits buildTxt date 1/11/2019 +tags index all +title Top Sinks replace NUM_SINKS 283 replace TABLE datatable @@ -16,8 +18,6 @@ replaceJs THE_DAY require("dayjs")(particle.parent.get("date")).format(`MMMM D, replaceJs TODAYS_DATE Date() replaceNodejs module.exports = {DIRNAME: __dirname} -tags index all -title Top Sinks header.scroll diff --git a/tests/txt.scroll b/tests/txt.scroll index 0067db9ac..04045ca9f 100644 --- a/tests/txt.scroll +++ b/tests/txt.scroll @@ -3,4 +3,4 @@ assert txt includes Hello assert txt excludes hola assert txt equals Hello world assert txt excludes < -assert html includes < \ No newline at end of file +assert html includes <