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 `
`
+ return ``
}
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 <